Compare commits
43 Commits
acp-thinki
...
v0.189.5-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac4bfede2e | ||
|
|
a9ac3734f6 | ||
|
|
69302bc7c7 | ||
|
|
3d272dbc2e | ||
|
|
72d4dd6ddc | ||
|
|
7382157c2f | ||
|
|
6241034ba1 | ||
|
|
414b3d46fa | ||
|
|
2e0f4acb40 | ||
|
|
ef301a529e | ||
|
|
dd25fa329c | ||
|
|
43a219afaf | ||
|
|
2926e78c03 | ||
|
|
0283d65f90 | ||
|
|
619d9ea134 | ||
|
|
79b472baa3 | ||
|
|
771a3acfe1 | ||
|
|
8541076cf9 | ||
|
|
c5499f49ee | ||
|
|
8757faf450 | ||
|
|
eece7266dd | ||
|
|
26b5ceabe6 | ||
|
|
60bbaaded7 | ||
|
|
fc061ab139 | ||
|
|
cf6ffab260 | ||
|
|
4932a127d1 | ||
|
|
7fef60004b | ||
|
|
51c8e35b2e | ||
|
|
f556d634fc | ||
|
|
4315869c62 | ||
|
|
cab167d53d | ||
|
|
6c86825756 | ||
|
|
0e562f2176 | ||
|
|
abea2409cd | ||
|
|
a5b511edb3 | ||
|
|
be1efa5d33 | ||
|
|
ce8ff9572e | ||
|
|
5ec3dffa11 | ||
|
|
261646656e | ||
|
|
0ac8774cd4 | ||
|
|
ba5c7326e9 | ||
|
|
41f944bd9e | ||
|
|
fc306a271c |
19
Cargo.lock
generated
@@ -559,6 +559,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4732,6 +4733,7 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"unindent",
|
||||
"url",
|
||||
@@ -5045,6 +5047,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6147,6 +6150,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -8929,6 +8933,7 @@ dependencies = [
|
||||
"async-compression",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"collections",
|
||||
"dap",
|
||||
"futures 0.3.31",
|
||||
@@ -8982,6 +8987,7 @@ dependencies = [
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -15581,6 +15587,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"hex",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
@@ -16480,9 +16487,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.3"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167"
|
||||
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -17115,8 +17122,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tendril",
|
||||
"unicase",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"util_macros",
|
||||
"walkdir",
|
||||
"workspace-hack",
|
||||
@@ -19680,7 +19685,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.189.0"
|
||||
version = "0.189.5"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -19876,9 +19881,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a8b9575b215536ed8ad254ba07171e4e13bd029eda3b54cca4b184d2768050"
|
||||
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -572,7 +572,7 @@ tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.5", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
@@ -617,7 +617,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.3"
|
||||
zed_llm_client = "0.8.4"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 715 B After Width: | Height: | Size: 617 B |
3
assets/icons/play_alt.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 3L13 8L4 13V3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
8
assets/icons/play_bug.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 12C2.35977 11.85 1 10.575 1 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.00875 15.2C1.00875 13.625 0.683456 12.275 4.00001 12.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 9C7 10.575 5.62857 11.85 4 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 12.2C6.98117 12.2 7 13.625 7 15.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="2.5" y="9" width="3" height="6" rx="1.5" fill="black"/>
|
||||
<path d="M9 10L13 8L4 3V7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 813 B |
@@ -1,3 +1,8 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 657 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 998 B |
@@ -31,8 +31,6 @@
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
@@ -582,11 +580,24 @@
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn",
|
||||
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }],
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
// or by tag:
|
||||
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
|
||||
"f5": "debugger::RerunLastSession"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_running",
|
||||
"bindings": {
|
||||
"f5": "zed::NoAction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_stopped",
|
||||
"bindings": {
|
||||
"f5": "debugger::Continue"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -872,7 +883,8 @@
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"ctrl-t": "debugger::ToggleThreadPicker",
|
||||
"ctrl-i": "debugger::ToggleSessionPicker"
|
||||
"ctrl-i": "debugger::ToggleSessionPicker",
|
||||
"shift-alt-escape": "debugger::ToggleExpandItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1018,5 +1030,12 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
@@ -634,7 +632,8 @@
|
||||
"cmd-k shift-right": "workspace::SwapPaneRight",
|
||||
"cmd-k shift-up": "workspace::SwapPaneUp",
|
||||
"cmd-k shift-down": "workspace::SwapPaneDown",
|
||||
"cmd-shift-x": "zed::Extensions"
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"f5": "debugger::RerunLastSession"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -651,6 +650,20 @@
|
||||
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_running",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f5": "zed::NoAction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && debugger_stopped",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f5": "debugger::Continue"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -935,7 +948,8 @@
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"cmd-t": "debugger::ToggleThreadPicker",
|
||||
"cmd-i": "debugger::ToggleSessionPicker"
|
||||
"cmd-i": "debugger::ToggleSessionPicker",
|
||||
"shift-alt-escape": "debugger::ToggleExpandItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1108,5 +1122,13 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1314,7 +1314,17 @@
|
||||
// Settings related to running tasks.
|
||||
"tasks": {
|
||||
"variables": {},
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
// Use LSP tasks over Zed language extension ones.
|
||||
// If no LSP tasks are returned due to error/timeout or regular execution,
|
||||
// Zed language extension tasks will be used instead.
|
||||
//
|
||||
// Other Zed tasks will still be shown:
|
||||
// * Zed task from either of the task config file
|
||||
// * Zed task from history (e.g. one-off task was spawned before)
|
||||
//
|
||||
// Default: true
|
||||
"prefer_lsp": true
|
||||
},
|
||||
// An object whose keys are language names, and whose values
|
||||
// are arrays of filenames or extensions of files that should
|
||||
@@ -1452,9 +1462,7 @@
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere",
|
||||
"preferred_line_length": 72,
|
||||
"soft_wrap": "bounded"
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Some example tasks for common languages.
|
||||
//
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[
|
||||
{
|
||||
"label": "Debug active PHP file",
|
||||
|
||||
5
assets/settings/initial_local_debug_tasks.json
Normal file
@@ -0,0 +1,5 @@
|
||||
// Project-local debug tasks
|
||||
//
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[]
|
||||
@@ -311,6 +311,31 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.find(|s| !s.read(cx).is_started())
|
||||
{
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
||||
let current_job = self
|
||||
.project
|
||||
.read(cx)
|
||||
|
||||
@@ -55,6 +55,7 @@ use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct ActiveThread {
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -1436,6 +1437,7 @@ impl ActiveThread {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
@@ -1597,7 +1599,12 @@ impl ActiveThread {
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this._load_edited_message_context_task = None;
|
||||
cx.notify();
|
||||
@@ -3691,7 +3698,8 @@ mod tests {
|
||||
|
||||
// Stream response to user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
let request = thread.to_completion_request(model.clone(), cx);
|
||||
let request =
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
|
||||
thread.stream_completion(request, model, cx.active_window(), cx)
|
||||
});
|
||||
// Follow the agent
|
||||
|
||||
@@ -52,7 +52,7 @@ use workspace::{
|
||||
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
||||
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
||||
@@ -1296,7 +1296,12 @@ impl AgentPanel {
|
||||
active_thread.thread().update(cx, |thread, cx| {
|
||||
thread.insert_invisible_continue_message(cx);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,7 @@ use std::{
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
@@ -464,6 +465,7 @@ impl CodegenAlternative {
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::InlineAssist),
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -42,6 +42,7 @@ use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -375,7 +376,12 @@ impl MessageEditor {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, Some(window_handle), cx);
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window_handle),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -1268,6 +1274,7 @@ impl MessageEditor {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
|
||||
@@ -25,6 +25,7 @@ use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -291,6 +292,7 @@ impl TerminalInlineAssistant {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: Some(CompletionIntent::TerminalInlineAssist),
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -38,7 +38,7 @@ use thiserror::Error;
|
||||
use ui::Window;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
||||
@@ -1187,6 +1187,7 @@ impl Thread {
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1196,7 +1197,7 @@ impl Thread {
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
let request = self.to_completion_request(model.clone(), cx);
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
|
||||
self.stream_completion(request, model, window, cx);
|
||||
}
|
||||
@@ -1216,11 +1217,13 @@ impl Thread {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
@@ -1374,12 +1377,14 @@ impl Thread {
|
||||
fn to_summarize_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
added_user_message: String,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
@@ -1857,7 +1862,12 @@ impl Thread {
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(
|
||||
&model.model,
|
||||
CompletionIntent::ThreadSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
|
||||
self.summary = ThreadSummary::Generating;
|
||||
|
||||
@@ -1958,7 +1968,12 @@ impl Thread {
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
|
||||
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(
|
||||
&model,
|
||||
CompletionIntent::ThreadContextSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
@@ -2050,7 +2065,8 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = Arc::new(self.to_completion_request(model.clone(), cx));
|
||||
let request =
|
||||
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
@@ -2246,7 +2262,7 @@ impl Thread {
|
||||
if self.all_tools_finished() {
|
||||
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
|
||||
if !canceled {
|
||||
self.send_to_model(model.clone(), window, cx);
|
||||
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
|
||||
}
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -2941,7 +2957,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3036,7 +3052,7 @@ fn main() {{
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// The request should contain all 3 messages
|
||||
@@ -3143,7 +3159,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3169,7 +3185,7 @@ fn main() {{
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
@@ -3214,7 +3230,7 @@ fn main() {{
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
@@ -3250,7 +3266,7 @@ fn main() {{
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
@@ -3300,7 +3316,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3320,7 +3336,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3340,7 +3356,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3360,7 +3376,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
@@ -3392,7 +3408,12 @@ fn main() {{
|
||||
// Send a message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3487,7 +3508,7 @@ fn main() {{
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3525,7 +3546,12 @@ fn main() {{
|
||||
) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
@@ -57,6 +57,7 @@ uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -45,6 +45,7 @@ use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
@@ -2272,6 +2273,7 @@ impl AssistantContext {
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::UserPrompt),
|
||||
mode: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
|
||||
@@ -155,53 +155,35 @@ impl LanguageModelSelector {
|
||||
}
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let mut recommended = Vec::new();
|
||||
let mut recommended_set = HashSet::default();
|
||||
for provider in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
|
||||
|
||||
let recommended = providers
|
||||
.iter()
|
||||
{
|
||||
let models = provider.recommended_models(cx);
|
||||
recommended_set.extend(models.iter().map(|model| (model.provider_id(), model.id())));
|
||||
recommended.extend(
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
.recommended_models(cx)
|
||||
.into_iter()
|
||||
.map(move |model| ModelInfo {
|
||||
model: model.clone(),
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let other_models = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
(
|
||||
provider.id(),
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.filter_map(|model| {
|
||||
let not_included =
|
||||
!recommended_set.contains(&(model.provider_id(), model.id()));
|
||||
not_included.then(|| ModelInfo {
|
||||
model: model.clone(),
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<IndexMap<_, _>>();
|
||||
.collect();
|
||||
|
||||
GroupedModels {
|
||||
recommended,
|
||||
other: other_models,
|
||||
}
|
||||
let other = providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(other, recommended)
|
||||
}
|
||||
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
@@ -326,11 +308,14 @@ struct GroupedModels {
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
|
||||
let recommended_ids = recommended
|
||||
.iter()
|
||||
.map(|info| (info.model.provider_id(), info.model.id()))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&model.model.id()) {
|
||||
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -917,4 +902,26 @@ mod tests {
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "claude"), // Should not be filtered out from "other"
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
||||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
@@ -106,7 +107,9 @@ impl EditAgent {
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let new_chunks = this.request(conversation, prompt, cx).await?;
|
||||
let new_chunks = this
|
||||
.request(conversation, CompletionIntent::CreateFile, prompt, cx)
|
||||
.await?;
|
||||
|
||||
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
|
||||
while let Some(event) = inner_events.next().await {
|
||||
@@ -213,7 +216,9 @@ impl EditAgent {
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let edit_chunks = this.request(conversation, prompt, cx).await?;
|
||||
let edit_chunks = this
|
||||
.request(conversation, CompletionIntent::EditFile, prompt, cx)
|
||||
.await?;
|
||||
this.apply_edit_chunks(buffer, edit_chunks, events_tx, cx)
|
||||
.await
|
||||
});
|
||||
@@ -589,6 +594,7 @@ impl EditAgent {
|
||||
async fn request(
|
||||
&self,
|
||||
mut conversation: LanguageModelRequest,
|
||||
intent: CompletionIntent,
|
||||
prompt: String,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
|
||||
@@ -646,6 +652,7 @@ impl EditAgent {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: conversation.thread_id,
|
||||
prompt_id: conversation.prompt_id,
|
||||
intent: Some(intent),
|
||||
mode: conversation.mode,
|
||||
messages: conversation.messages,
|
||||
tool_choice,
|
||||
|
||||
@@ -298,6 +298,7 @@ pub async fn download_adapter_from_github(
|
||||
response.status().to_string()
|
||||
);
|
||||
|
||||
delegate.output_to_console("Download complete".to_owned());
|
||||
match file_type {
|
||||
DownloadedFileType::GzipTar => {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
@@ -369,21 +370,19 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
/// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
|
||||
/// This method should only return error when the kind cannot be determined for a given configuration;
|
||||
/// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
|
||||
fn request_kind(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
let map = config.as_object().context("Config isn't an object")?;
|
||||
|
||||
let request_variant = map
|
||||
.get("request")
|
||||
.and_then(|val| val.as_str())
|
||||
.context("request argument is not found or invalid")?;
|
||||
|
||||
match request_variant {
|
||||
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
|
||||
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
|
||||
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
|
||||
match config.get("request") {
|
||||
Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
|
||||
Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
|
||||
_ => Err(anyhow!(
|
||||
"missing or invalid `request` field in config. Expected 'launch' or 'attach'"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +412,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
fn request_kind(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
@@ -458,7 +457,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
envs: HashMap::default(),
|
||||
cwd: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.validate_config(&task_definition.config)?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
configuration: task_definition.config.clone(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation
|
||||
return;
|
||||
};
|
||||
let kind = adapter
|
||||
.validate_config(&scenario.config)
|
||||
.request_kind(&scenario.config)
|
||||
.ok()
|
||||
.map(serde_json::to_value)
|
||||
.and_then(Result::ok);
|
||||
|
||||
@@ -4,7 +4,7 @@ use dap_types::{
|
||||
messages::{Message, Response},
|
||||
};
|
||||
use futures::{AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _, channel::oneshot, select};
|
||||
use gpui::AsyncApp;
|
||||
use gpui::{AppContext as _, AsyncApp, Task};
|
||||
use settings::Settings as _;
|
||||
use smallvec::SmallVec;
|
||||
use smol::{
|
||||
@@ -22,7 +22,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use util::{ConnectionResult, ResultExt as _};
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
@@ -126,7 +126,7 @@ pub(crate) struct TransportDelegate {
|
||||
pending_requests: Requests,
|
||||
transport: Transport,
|
||||
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
|
||||
_tasks: Vec<gpui::Task<Option<()>>>,
|
||||
_tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl TransportDelegate {
|
||||
@@ -141,7 +141,7 @@ impl TransportDelegate {
|
||||
log_handlers: Default::default(),
|
||||
current_requests: Default::default(),
|
||||
pending_requests: Default::default(),
|
||||
_tasks: Default::default(),
|
||||
_tasks: Vec::new(),
|
||||
};
|
||||
let messages = this.start_handlers(transport_pipes, cx).await?;
|
||||
Ok((messages, this))
|
||||
@@ -166,45 +166,76 @@ impl TransportDelegate {
|
||||
None
|
||||
};
|
||||
|
||||
let adapter_log_handler = log_handler.clone();
|
||||
cx.update(|cx| {
|
||||
if let Some(stdout) = params.stdout.take() {
|
||||
self._tasks.push(
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
|
||||
);
|
||||
self._tasks.push(cx.background_spawn(async move {
|
||||
match Self::handle_adapter_log(stdout, adapter_log_handler).await {
|
||||
ConnectionResult::Timeout => {
|
||||
log::error!("Timed out when handling debugger log");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger logs connection closed");
|
||||
}
|
||||
ConnectionResult::Result(Ok(())) => {}
|
||||
ConnectionResult::Result(Err(e)) => {
|
||||
log::error!("Error handling debugger log: {e}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
self._tasks.push(
|
||||
cx.background_executor().spawn(
|
||||
Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
)
|
||||
.log_err(),
|
||||
),
|
||||
);
|
||||
let pending_requests = self.pending_requests.clone();
|
||||
let output_log_handler = log_handler.clone();
|
||||
self._tasks.push(cx.background_spawn(async move {
|
||||
match Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
pending_requests,
|
||||
output_log_handler,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("Error handling debugger output: {e}"),
|
||||
}
|
||||
}));
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
self._tasks.push(
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
|
||||
);
|
||||
let log_handlers = self.log_handlers.clone();
|
||||
self._tasks.push(cx.background_spawn(async move {
|
||||
match Self::handle_error(stderr, log_handlers).await {
|
||||
ConnectionResult::Timeout => {
|
||||
log::error!("Timed out reading debugger error stream")
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed its error stream")
|
||||
}
|
||||
ConnectionResult::Result(Ok(())) => {}
|
||||
ConnectionResult::Result(Err(e)) => {
|
||||
log::error!("Error handling debugger error: {e}")
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
self._tasks.push(
|
||||
cx.background_executor().spawn(
|
||||
Self::handle_input(
|
||||
params.input,
|
||||
client_rx,
|
||||
self.current_requests.clone(),
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
)
|
||||
.log_err(),
|
||||
),
|
||||
);
|
||||
let current_requests = self.current_requests.clone();
|
||||
let pending_requests = self.pending_requests.clone();
|
||||
let log_handler = log_handler.clone();
|
||||
self._tasks.push(cx.background_spawn(async move {
|
||||
match Self::handle_input(
|
||||
params.input,
|
||||
client_rx,
|
||||
current_requests,
|
||||
pending_requests,
|
||||
log_handler,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("Error handling debugger input: {e}"),
|
||||
}
|
||||
}));
|
||||
})?;
|
||||
|
||||
{
|
||||
@@ -235,7 +266,7 @@ impl TransportDelegate {
|
||||
async fn handle_adapter_log<Stdout>(
|
||||
stdout: Stdout,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
) -> ConnectionResult<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
@@ -245,13 +276,14 @@ impl TransportDelegate {
|
||||
let result = loop {
|
||||
line.truncate(0);
|
||||
|
||||
let bytes_read = match reader.read_line(&mut line).await {
|
||||
Ok(bytes_read) => bytes_read,
|
||||
Err(e) => break Err(e.into()),
|
||||
};
|
||||
|
||||
if bytes_read == 0 {
|
||||
anyhow::bail!("Debugger log stream closed");
|
||||
match reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.context("reading adapter log line")
|
||||
{
|
||||
Ok(0) => break ConnectionResult::ConnectionReset,
|
||||
Ok(_) => {}
|
||||
Err(e) => break ConnectionResult::Result(Err(e)),
|
||||
}
|
||||
|
||||
if let Some(log_handlers) = log_handlers.as_ref() {
|
||||
@@ -337,35 +369,35 @@ impl TransportDelegate {
|
||||
let mut reader = BufReader::new(server_stdout);
|
||||
|
||||
let result = loop {
|
||||
let message =
|
||||
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
|
||||
.await;
|
||||
|
||||
match message {
|
||||
Ok(Message::Response(res)) => {
|
||||
match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
|
||||
.await
|
||||
{
|
||||
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
return Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Ok(Message::Response(res))) => {
|
||||
if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
|
||||
if let Err(e) = tx.send(Self::process_response(res)) {
|
||||
log::trace!("Did not send response `{:?}` for a cancelled", e);
|
||||
}
|
||||
} else {
|
||||
client_tx.send(Message::Response(res)).await?;
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(message) => {
|
||||
client_tx.send(message).await?;
|
||||
}
|
||||
Err(e) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => client_tx.send(message).await?,
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
}
|
||||
};
|
||||
|
||||
drop(client_tx);
|
||||
|
||||
log::debug!("Handle adapter output dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()>
|
||||
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> ConnectionResult<()>
|
||||
where
|
||||
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
@@ -375,8 +407,12 @@ impl TransportDelegate {
|
||||
let mut reader = BufReader::new(stderr);
|
||||
|
||||
let result = loop {
|
||||
match reader.read_line(&mut buffer).await {
|
||||
Ok(0) => anyhow::bail!("debugger error stream closed"),
|
||||
match reader
|
||||
.read_line(&mut buffer)
|
||||
.await
|
||||
.context("reading error log line")
|
||||
{
|
||||
Ok(0) => break ConnectionResult::ConnectionReset,
|
||||
Ok(_) => {
|
||||
for (kind, log_handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Adapter) {
|
||||
@@ -386,7 +422,7 @@ impl TransportDelegate {
|
||||
|
||||
buffer.truncate(0);
|
||||
}
|
||||
Err(error) => break Err(error.into()),
|
||||
Err(error) => break ConnectionResult::Result(Err(error)),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -420,7 +456,7 @@ impl TransportDelegate {
|
||||
reader: &mut BufReader<Stdout>,
|
||||
buffer: &mut String,
|
||||
log_handlers: Option<&LogHandlers>,
|
||||
) -> Result<Message>
|
||||
) -> ConnectionResult<Message>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
@@ -428,48 +464,58 @@ impl TransportDelegate {
|
||||
loop {
|
||||
buffer.truncate(0);
|
||||
|
||||
if reader
|
||||
match reader
|
||||
.read_line(buffer)
|
||||
.await
|
||||
.with_context(|| "reading a message from server")?
|
||||
== 0
|
||||
.with_context(|| "reading a message from server")
|
||||
{
|
||||
anyhow::bail!("debugger reader stream closed");
|
||||
Ok(0) => return ConnectionResult::ConnectionReset,
|
||||
Ok(_) => {}
|
||||
Err(e) => return ConnectionResult::Result(Err(e)),
|
||||
};
|
||||
|
||||
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")?);
|
||||
if let Some(("Content-Length", value)) = buffer.trim().split_once(": ") {
|
||||
match value.parse().context("invalid content length") {
|
||||
Ok(length) => content_length = Some(length),
|
||||
Err(e) => return ConnectionResult::Result(Err(e)),
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
let content_length = match content_length.context("missing content length") {
|
||||
Ok(length) => length,
|
||||
Err(e) => return ConnectionResult::Result(Err(e)),
|
||||
};
|
||||
|
||||
let mut content = vec![0; content_length];
|
||||
reader
|
||||
if let Err(e) = reader
|
||||
.read_exact(&mut content)
|
||||
.await
|
||||
.with_context(|| "reading after a loop")?;
|
||||
.with_context(|| "reading after a loop")
|
||||
{
|
||||
return ConnectionResult::Result(Err(e));
|
||||
}
|
||||
|
||||
let message = std::str::from_utf8(&content).context("invalid utf8 from server")?;
|
||||
let message_str = match std::str::from_utf8(&content).context("invalid utf8 from server") {
|
||||
Ok(str) => str,
|
||||
Err(e) => return ConnectionResult::Result(Err(e)),
|
||||
};
|
||||
|
||||
if let Some(log_handlers) = log_handlers {
|
||||
for (kind, log_handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Rpc) {
|
||||
log_handler(IoKind::StdOut, &message);
|
||||
log_handler(IoKind::StdOut, message_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str::<Message>(message)?)
|
||||
ConnectionResult::Result(
|
||||
serde_json::from_str::<Message>(message_str).context("deserializing server message"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
@@ -658,9 +704,13 @@ impl StdioTransport {
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
let mut process = command.spawn().with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command `{} {}`.",
|
||||
binary.command,
|
||||
binary.arguments.join(" ")
|
||||
)
|
||||
})?;
|
||||
|
||||
let stdin = process.stdin.take().context("Failed to open stdin")?;
|
||||
let stdout = process.stdout.take().context("Failed to open stdout")?;
|
||||
@@ -773,71 +823,31 @@ impl FakeTransport {
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
let stdout_writer = Arc::new(Mutex::new(stdout_writer));
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let mut buffer = String::new();
|
||||
cx.background_spawn(async move {
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let mut buffer = String::new();
|
||||
|
||||
loop {
|
||||
let message =
|
||||
TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
|
||||
.await;
|
||||
|
||||
match message {
|
||||
Err(error) => {
|
||||
break anyhow::anyhow!(error);
|
||||
}
|
||||
Ok(message) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Request(request))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
let response = if let Some(handle) = request_handlers
|
||||
.lock()
|
||||
.get_mut(request.command.as_str())
|
||||
{
|
||||
handle(
|
||||
request.seq,
|
||||
request.arguments.unwrap_or(json!({})),
|
||||
)
|
||||
} else {
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
loop {
|
||||
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
|
||||
.await
|
||||
{
|
||||
ConnectionResult::Timeout => {
|
||||
anyhow::bail!("Timed out when connecting to debugger");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
serde_json::to_string(&Message::Request(request)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
@@ -848,22 +858,58 @@ impl FakeTransport {
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
} else {
|
||||
let response = if let Some(handle) =
|
||||
request_handlers.lock().get_mut(request.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
handle(request.seq, request.arguments.unwrap_or(json!({})))
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
let message =
|
||||
serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message).as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok((
|
||||
TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None),
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use dap::{
|
||||
StartDebuggingRequestArgumentsRequest,
|
||||
adapters::{DebugTaskDefinition, latest_github_release},
|
||||
};
|
||||
use dap::adapters::{DebugTaskDefinition, latest_github_release};
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
@@ -37,7 +34,7 @@ impl CodeLldbDebugAdapter {
|
||||
Value::String(String::from(task_definition.label.as_ref())),
|
||||
);
|
||||
|
||||
let request = self.validate_config(&configuration)?;
|
||||
let request = self.request_kind(&configuration)?;
|
||||
|
||||
Ok(dap::StartDebuggingRequestArguments {
|
||||
request,
|
||||
@@ -89,48 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
let map = config
|
||||
.as_object()
|
||||
.ok_or_else(|| anyhow!("Config isn't an object"))?;
|
||||
|
||||
let request_variant = map
|
||||
.get("request")
|
||||
.and_then(|r| r.as_str())
|
||||
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
|
||||
|
||||
match request_variant {
|
||||
"launch" => {
|
||||
// For launch, verify that one of the required configs exists
|
||||
if !(map.contains_key("program")
|
||||
|| map.contains_key("targetCreateCommands")
|
||||
|| map.contains_key("cargo"))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
|
||||
));
|
||||
}
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
"attach" => {
|
||||
// For attach, verify that either pid or program exists
|
||||
if !(map.contains_key("pid") || map.contains_key("program")) {
|
||||
return Err(anyhow!(
|
||||
"attach request requires either 'pid' or 'program' field"
|
||||
));
|
||||
}
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Attach)
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"request must be either 'launch' or 'attach', got '{}'",
|
||||
request_variant
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut configuration = json!({
|
||||
"request": match zed_scenario.request {
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn init(cx: &mut App) {
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(RubyDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
@@ -178,7 +178,7 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
|
||||
|
||||
let request_args = StartDebuggingRequestArguments {
|
||||
request: self.validate_config(&config.config)?,
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration: config.config.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,87 @@
|
||||
use anyhow::{Context as _, anyhow, bail};
|
||||
use anyhow::{Context as _, bail};
|
||||
use dap::{
|
||||
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::DebugTaskDefinition,
|
||||
StartDebuggingRequestArguments,
|
||||
adapters::{
|
||||
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
|
||||
latest_github_release,
|
||||
},
|
||||
};
|
||||
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use util;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct GoDebugAdapter;
|
||||
pub(crate) struct GoDebugAdapter {
|
||||
shim_path: OnceLock<PathBuf>,
|
||||
}
|
||||
|
||||
impl GoDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Delve";
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 60000;
|
||||
async fn fetch_latest_adapter_version(
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&"zed-industries/delve-shim-dap",
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let os = match consts::OS {
|
||||
"macos" => "apple-darwin",
|
||||
"linux" => "unknown-linux-gnu",
|
||||
"windows" => "pc-windows-msvc",
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
let suffix = if consts::OS == "windows" {
|
||||
".zip"
|
||||
} else {
|
||||
".tar.gz"
|
||||
};
|
||||
let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
})
|
||||
}
|
||||
async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
|
||||
if let Some(path) = self.shim_path.get().cloned() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let asset = Self::fetch_latest_adapter_version(delegate).await?;
|
||||
let ty = if consts::OS == "windows" {
|
||||
DownloadedFileType::Zip
|
||||
} else {
|
||||
DownloadedFileType::GzipTar
|
||||
};
|
||||
download_adapter_from_github(
|
||||
"delve-shim-dap".into(),
|
||||
asset.clone(),
|
||||
ty,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = paths::debug_adapters_dir()
|
||||
.join("delve-shim-dap")
|
||||
.join(format!("delve-shim-dap_{}", asset.tag_name))
|
||||
.join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
|
||||
self.shim_path.set(path.clone()).ok();
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -285,24 +350,6 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
let map = config.as_object().context("Config isn't an object")?;
|
||||
|
||||
let request_variant = map
|
||||
.get("request")
|
||||
.and_then(|val| val.as_str())
|
||||
.context("request argument is not found or invalid")?;
|
||||
|
||||
match request_variant {
|
||||
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
|
||||
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
|
||||
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut args = match &zed_scenario.request {
|
||||
dap::DebugRequest::Attach(attach_config) => {
|
||||
@@ -349,13 +396,15 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
||||
let dlv_path = adapter_path.join("dlv");
|
||||
|
||||
let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
|
||||
let delve_path = if let Some(path) = user_installed_path {
|
||||
path.to_string_lossy().to_string()
|
||||
} else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
|
||||
path.to_string_lossy().to_string()
|
||||
} else if delegate.fs().is_file(&dlv_path).await {
|
||||
dlv_path.to_string_lossy().to_string()
|
||||
@@ -384,16 +433,10 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
|
||||
adapter_path.join("dlv").to_string_lossy().to_string()
|
||||
};
|
||||
let minidelve_path = self.install_shim(delegate).await?;
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
|
||||
let mut tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
|
||||
if tcp_connection.timeout.is_none()
|
||||
|| tcp_connection.timeout.unwrap_or(0) < Self::DEFAULT_TIMEOUT_MS
|
||||
{
|
||||
tcp_connection.timeout = Some(Self::DEFAULT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let cwd = task_definition
|
||||
.config
|
||||
@@ -404,6 +447,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
|
||||
let arguments = if cfg!(windows) {
|
||||
vec![
|
||||
delve_path,
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", host, port),
|
||||
@@ -411,6 +455,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
delve_path,
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", host, port),
|
||||
@@ -418,18 +463,14 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delve_path,
|
||||
command: minidelve_path.to_string_lossy().into_owned(),
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
connection: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
request: self.validate_config(&task_definition.config)?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use adapters::latest_github_release;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use dap::{
|
||||
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::DebugTaskDefinition,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
@@ -26,7 +23,7 @@ impl JsDebugAdapter {
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
|
||||
&format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
@@ -95,7 +92,7 @@ impl JsDebugAdapter {
|
||||
}),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
request: self.validate_config(&task_definition.config)?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -107,29 +104,6 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
|
||||
match config.get("request") {
|
||||
Some(val) if val == "launch" => {
|
||||
if config.get("program").is_none() && config.get("url").is_none() {
|
||||
return Err(anyhow!(
|
||||
"either program or url is required for launch request"
|
||||
));
|
||||
}
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
Some(val) if val == "attach" => {
|
||||
if !config.get("processId").is_some_and(|val| val.is_u64()) {
|
||||
return Err(anyhow!("processId must be a number"));
|
||||
}
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Attach)
|
||||
}
|
||||
_ => Err(anyhow!("missing or invalid request field in config")),
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut args = json!({
|
||||
"type": "pwa-node",
|
||||
@@ -449,6 +423,8 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ impl PhpDebugAdapter {
|
||||
envs: HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
request: <Self as DebugAdapter>::validate_config(self, &task_definition.config)?,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -149,22 +149,8 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
"default": false
|
||||
},
|
||||
"pathMappings": {
|
||||
"type": "array",
|
||||
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serverPath": {
|
||||
"type": "string",
|
||||
"description": "Path on the server"
|
||||
},
|
||||
"localPath": {
|
||||
"type": "string",
|
||||
"description": "Corresponding path on the local machine"
|
||||
}
|
||||
},
|
||||
"required": ["serverPath", "localPath"]
|
||||
}
|
||||
"type": "object",
|
||||
"description": "A mapping of server paths to local paths.",
|
||||
},
|
||||
"log": {
|
||||
"type": "boolean",
|
||||
@@ -296,10 +282,7 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
Some(SharedString::new_static("PHP").into())
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::*;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::DebugTaskDefinition,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use json_dotpath::DotPaths;
|
||||
use language::{LanguageName, Toolchain};
|
||||
@@ -86,7 +83,7 @@ impl PythonDebugAdapter {
|
||||
&self,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<StartDebuggingRequestArguments> {
|
||||
let request = self.validate_config(&task_definition.config)?;
|
||||
let request = self.request_kind(&task_definition.config)?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Ok(console) = configuration.dot_get_mut("console") {
|
||||
@@ -254,24 +251,6 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_config(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
let map = config.as_object().context("Config isn't an object")?;
|
||||
|
||||
let request_variant = map
|
||||
.get("request")
|
||||
.and_then(|val| val.as_str())
|
||||
.context("request is not valid")?;
|
||||
|
||||
match request_variant {
|
||||
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
|
||||
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
|
||||
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"properties": {
|
||||
@@ -660,7 +639,7 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
self.get_installed_binary(delegate, &config, None, None, false)
|
||||
self.get_installed_binary(delegate, &config, None, toolchain, false)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
cwd: None,
|
||||
envs: std::collections::HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.validate_config(&definition.config)?,
|
||||
request: self.request_kind(&definition.config)?,
|
||||
configuration: definition.config.clone(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ project.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
# serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
sysinfo.workspace = true
|
||||
|
||||
@@ -3,11 +3,12 @@ use crate::session::DebugSession;
|
||||
use crate::session::running::RunningState;
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
|
||||
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
|
||||
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
|
||||
persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::Result;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
@@ -24,7 +25,7 @@ use gpui::{
|
||||
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Fs, ProjectPath, WorktreeId};
|
||||
use project::{Fs, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
@@ -65,9 +66,11 @@ pub struct DebugPanel {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
debug_scenario_scheduled_last: bool,
|
||||
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_zoomed: bool,
|
||||
_subscriptions: [Subscription; 1],
|
||||
}
|
||||
|
||||
@@ -102,7 +105,9 @@ impl DebugPanel {
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
thread_picker_menu_handle,
|
||||
session_picker_menu_handle,
|
||||
is_zoomed: false,
|
||||
_subscriptions: [focus_subscription],
|
||||
debug_scenario_scheduled_last: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -264,6 +269,7 @@ impl DebugPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.debug_scenario_scheduled_last = true;
|
||||
if let Some(inventory) = self
|
||||
.project
|
||||
.read(cx)
|
||||
@@ -331,10 +337,17 @@ impl DebugPanel {
|
||||
let Some(task_inventory) = task_store.read(cx).task_inventory() else {
|
||||
return;
|
||||
};
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
|
||||
window.defer(cx, move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Launch, None, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
return;
|
||||
};
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_contexts = workspace
|
||||
@@ -432,7 +445,10 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = parent_session.read(cx).label().clone();
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
@@ -936,68 +952,69 @@ impl DebugPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn save_scenario(
|
||||
&self,
|
||||
scenario: &DebugScenario,
|
||||
worktree_id: WorktreeId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ProjectPath>> {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
|
||||
};
|
||||
// TODO: restore once we have proper comment preserving file edits
|
||||
// pub(crate) fn save_scenario(
|
||||
// &self,
|
||||
// scenario: &DebugScenario,
|
||||
// worktree_id: WorktreeId,
|
||||
// window: &mut Window,
|
||||
// cx: &mut App,
|
||||
// ) -> Task<Result<ProjectPath>> {
|
||||
// self.workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
|
||||
// };
|
||||
|
||||
let serialized_scenario = serde_json::to_value(scenario);
|
||||
// let serialized_scenario = serde_json::to_value(scenario);
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let serialized_scenario = serialized_scenario?;
|
||||
let fs =
|
||||
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
// cx.spawn_in(window, async move |workspace, cx| {
|
||||
// let serialized_scenario = serialized_scenario?;
|
||||
// let fs =
|
||||
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
|
||||
path.push(paths::local_settings_folder_relative_path());
|
||||
if !fs.is_dir(path.as_path()).await {
|
||||
fs.create_dir(path.as_path()).await?;
|
||||
}
|
||||
path.pop();
|
||||
// path.push(paths::local_settings_folder_relative_path());
|
||||
// if !fs.is_dir(path.as_path()).await {
|
||||
// fs.create_dir(path.as_path()).await?;
|
||||
// }
|
||||
// path.pop();
|
||||
|
||||
path.push(paths::local_debug_file_relative_path());
|
||||
let path = path.as_path();
|
||||
// path.push(paths::local_debug_file_relative_path());
|
||||
// let path = path.as_path();
|
||||
|
||||
if !fs.is_file(path).await {
|
||||
let content =
|
||||
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
|
||||
serialized_scenario,
|
||||
]))?;
|
||||
// if !fs.is_file(path).await {
|
||||
// fs.create_file(path, Default::default()).await?;
|
||||
// fs.write(
|
||||
// path,
|
||||
// initial_local_debug_tasks_content().to_string().as_bytes(),
|
||||
// )
|
||||
// .await?;
|
||||
// }
|
||||
|
||||
fs.create_file(path, Default::default()).await?;
|
||||
fs.save(path, &content.into(), Default::default()).await?;
|
||||
} else {
|
||||
let content = fs.load(path).await?;
|
||||
let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
|
||||
values.push(serialized_scenario);
|
||||
fs.save(
|
||||
path,
|
||||
&serde_json::to_string_pretty(&values).map(Into::into)?,
|
||||
Default::default(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// let content = fs.load(path).await?;
|
||||
// let mut values =
|
||||
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
|
||||
// values.push(serialized_scenario);
|
||||
// fs.save(
|
||||
// path,
|
||||
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
|
||||
// Default::default(),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context(
|
||||
"Couldn't get project path for .zed/debug.json in active worktree",
|
||||
)
|
||||
})?
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
}
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// workspace
|
||||
// .project()
|
||||
// .read(cx)
|
||||
// .project_path_for_absolute_path(&path, cx)
|
||||
// .context(
|
||||
// "Couldn't get project path for .zed/debug.json in active worktree",
|
||||
// )
|
||||
// })?
|
||||
// })
|
||||
// })
|
||||
// .unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
// }
|
||||
|
||||
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_picker_menu_handle.toggle(window, cx);
|
||||
@@ -1006,6 +1023,22 @@ impl DebugPanel {
|
||||
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.session_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
fn toggle_zoom(
|
||||
&mut self,
|
||||
_: &workspace::ToggleZoom,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.is_zoomed {
|
||||
cx.emit(PanelEvent::ZoomOut);
|
||||
} else {
|
||||
if !self.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_session_inner(
|
||||
@@ -1161,6 +1194,15 @@ impl Panel for DebugPanel {
|
||||
}
|
||||
|
||||
fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
|
||||
|
||||
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
|
||||
self.is_zoomed
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.is_zoomed = zoomed;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
@@ -1301,6 +1343,23 @@ impl Render for DebugPanel {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(cx.listener(Self::toggle_zoom))
|
||||
.on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
|
||||
let Some(session) = panel.active_session() else {
|
||||
return;
|
||||
};
|
||||
let active_pane = session
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.clone();
|
||||
active_pane.update(cx, |pane, cx| {
|
||||
let is_zoomed = pane.is_zoomed();
|
||||
pane.set_zoomed(!is_zoomed, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
.when(self.active_session.is_some(), |this| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
@@ -1378,4 +1437,36 @@ impl workspace::DebuggerProvider for DebuggerProvider {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
action: &tasks_ui::Spawn,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
spawn_task_or_modal(workspace, action, window, cx);
|
||||
}
|
||||
|
||||
fn debug_scenario_scheduled(&self, cx: &mut App) {
|
||||
self.0.update(cx, |this, _| {
|
||||
this.debug_scenario_scheduled_last = true;
|
||||
});
|
||||
}
|
||||
|
||||
fn task_scheduled(&self, cx: &mut App) {
|
||||
self.0.update(cx, |this, _| {
|
||||
this.debug_scenario_scheduled_last = false;
|
||||
})
|
||||
}
|
||||
|
||||
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
|
||||
self.0.read(cx).debug_scenario_scheduled_last
|
||||
}
|
||||
|
||||
fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
|
||||
let session = self.0.read(cx).active_session()?;
|
||||
let thread = session.read(cx).running_state().read(cx).thread_id()?;
|
||||
session.read(cx).session(cx).read(cx).thread_state(thread)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, EntityInputHandler, actions};
|
||||
use new_session_modal::NewSessionModal;
|
||||
use new_process_modal::{NewProcessModal, NewProcessMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use stack_trace_view::StackTraceView;
|
||||
use tasks_ui::{Spawn, TaskOverrides};
|
||||
use util::maybe;
|
||||
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
pub mod attach_modal;
|
||||
pub mod debugger_panel;
|
||||
mod dropdown_menus;
|
||||
mod new_session_modal;
|
||||
mod new_process_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
mod stack_trace_view;
|
||||
@@ -48,6 +49,7 @@ actions!(
|
||||
ToggleThreadPicker,
|
||||
ToggleSessionPicker,
|
||||
RerunLastSession,
|
||||
ToggleExpandItem,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -62,6 +64,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
|
||||
workspace
|
||||
.register_action(spawn_task_or_modal)
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
@@ -208,7 +211,7 @@ pub fn init(cx: &mut App) {
|
||||
},
|
||||
)
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewSessionModal::show(workspace, window, cx);
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
@@ -309,3 +312,48 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
workspace: &mut Workspace,
|
||||
action: &Spawn,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Workspace>,
|
||||
) {
|
||||
match action {
|
||||
Spawn::ByName {
|
||||
task_name,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let name = task_name.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.label.eq(&name),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ByTag {
|
||||
task_tag,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let tag = task_tag.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.tags.contains(&tag),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ViaModal { reveal_target } => {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::Entity;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
|
||||
@@ -23,31 +25,40 @@ impl DebugPanel {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session {
|
||||
let label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let session_state_indicator = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Conflict).into_any_element()
|
||||
}
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.when_some(session_state_indicator, |this, indicator| {
|
||||
this.child(indicator)
|
||||
})
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
|
||||
@@ -61,6 +61,28 @@ impl DebuggerPaneItem {
|
||||
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
|
||||
}
|
||||
}
|
||||
pub(crate) fn tab_tooltip(self) -> SharedString {
|
||||
let tooltip = match self {
|
||||
DebuggerPaneItem::Console => {
|
||||
"Displays program output and allows manual input of debugger commands."
|
||||
}
|
||||
DebuggerPaneItem::Variables => {
|
||||
"Shows current values of local and global variables in the current stack frame."
|
||||
}
|
||||
DebuggerPaneItem::BreakpointList => "Lists all active breakpoints set in the code.",
|
||||
DebuggerPaneItem::Frames => {
|
||||
"Displays the call stack, letting you navigate between function calls."
|
||||
}
|
||||
DebuggerPaneItem::Modules => "Shows all modules or libraries loaded by the program.",
|
||||
DebuggerPaneItem::LoadedSources => {
|
||||
"Lists all source files currently loaded and used by the debugger."
|
||||
}
|
||||
DebuggerPaneItem::Terminal => {
|
||||
"Provides an interactive terminal session within the debugging environment."
|
||||
}
|
||||
};
|
||||
SharedString::new_static(tooltip)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DebuggerPaneItem> for SharedString {
|
||||
|
||||
@@ -8,7 +8,8 @@ pub mod variable_list;
|
||||
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
new_session_modal::resolve_path,
|
||||
ToggleExpandItem,
|
||||
new_process_modal::resolve_path,
|
||||
persistence::{self, DebuggerPaneItem, SerializedLayout},
|
||||
};
|
||||
|
||||
@@ -173,6 +174,10 @@ impl Item for SubView {
|
||||
self.kind.to_shared_string()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
Some(self.kind.tab_tooltip())
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
@@ -343,6 +348,7 @@ pub(crate) fn new_debugger_pane(
|
||||
false
|
||||
}
|
||||
})));
|
||||
pane.set_can_toggle_zoom(false, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_custom_drop_handle(cx, custom_drop_handle);
|
||||
pane.set_should_display_tab_bar(|_, _| true);
|
||||
@@ -399,6 +405,9 @@ pub(crate) fn new_debugger_pane(
|
||||
.p_1()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.when_some(item.tab_tooltip_text(cx), |this, tooltip| {
|
||||
this.tooltip(Tooltip::text(tooltip))
|
||||
})
|
||||
.map(|this| {
|
||||
let theme = cx.theme();
|
||||
if selected {
|
||||
@@ -465,17 +474,19 @@ pub(crate) fn new_debugger_pane(
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(move |pane, _, window, cx| {
|
||||
pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
|
||||
.on_click(cx.listener(move |pane, _, _, cx| {
|
||||
let is_zoomed = pane.is_zoomed();
|
||||
pane.set_zoomed(!is_zoomed, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let zoomed_text =
|
||||
if zoomed { "Zoom Out" } else { "Zoom In" };
|
||||
if zoomed { "Minimize" } else { "Expand" };
|
||||
Tooltip::for_action_in(
|
||||
zoomed_text,
|
||||
&workspace::ToggleZoom,
|
||||
&ToggleExpandItem,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -559,7 +570,7 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn relativlize_paths(
|
||||
pub(crate) fn relativize_paths(
|
||||
key: Option<&str>,
|
||||
config: &mut serde_json::Value,
|
||||
context: &TaskContext,
|
||||
@@ -567,12 +578,12 @@ impl RunningState {
|
||||
match config {
|
||||
serde_json::Value::Object(obj) => {
|
||||
obj.iter_mut()
|
||||
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
|
||||
.for_each(|(key, value)| Self::relativize_paths(Some(key), value, context));
|
||||
}
|
||||
serde_json::Value::Array(array) => {
|
||||
array
|
||||
.iter_mut()
|
||||
.for_each(|value| Self::relativlize_paths(None, value, context));
|
||||
.for_each(|value| Self::relativize_paths(None, value, context));
|
||||
}
|
||||
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
|
||||
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
|
||||
@@ -799,13 +810,13 @@ impl RunningState {
|
||||
mut config,
|
||||
tcp_connection,
|
||||
} = scenario;
|
||||
Self::relativlize_paths(None, &mut config, &task_context);
|
||||
Self::relativize_paths(None, &mut config, &task_context);
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
|
||||
let request_type = dap_registry
|
||||
.adapter(&adapter)
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.and_then(|adapter| adapter.validate_config(&config));
|
||||
.and_then(|adapter| adapter.request_kind(&config));
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
@@ -874,7 +885,6 @@ impl RunningState {
|
||||
args,
|
||||
..task.resolved.clone()
|
||||
};
|
||||
|
||||
let terminal = project
|
||||
.update_in(cx, |project, window, cx| {
|
||||
project.create_terminal(
|
||||
@@ -919,6 +929,12 @@ impl RunningState {
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
// Ok(DebugTaskDefinition {
|
||||
// label,
|
||||
// adapter: DebugAdapterName(adapter),
|
||||
// config,
|
||||
// tcp_connection,
|
||||
// })
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.context("Could not find a valid locator for a build task")?;
|
||||
@@ -937,12 +953,15 @@ impl RunningState {
|
||||
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.context(format!("{}: is not a valid adapter name", &adapter))
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
|
||||
config = scenario.config;
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
} else {
|
||||
anyhow::bail!("No request or build provided");
|
||||
let Err(e) = request_type else {
|
||||
unreachable!();
|
||||
};
|
||||
anyhow::bail!("Zed cannot determine how to run this debug scenario. `build` field was not provided and Debug Adapter won't accept provided configuration because: {e}");
|
||||
};
|
||||
|
||||
Ok(DebugTaskDefinition {
|
||||
@@ -1245,18 +1264,6 @@ impl RunningState {
|
||||
Event::Focus => {
|
||||
this.active_pane = source_pane.clone();
|
||||
}
|
||||
Event::ZoomIn => {
|
||||
source_pane.update(cx, |pane, cx| {
|
||||
pane.set_zoomed(true, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
Event::ZoomOut => {
|
||||
source_pane.update(cx, |pane, cx| {
|
||||
pane.set_zoomed(false, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Console {
|
||||
}
|
||||
|
||||
fn is_running(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_local()
|
||||
self.session.read(cx).is_running()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
@@ -176,16 +176,18 @@ impl Console {
|
||||
}
|
||||
|
||||
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(&self.console, self.editor_style(cx))
|
||||
EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
|
||||
}
|
||||
|
||||
fn editor_style(&self, cx: &Context<Self>) -> EditorStyle {
|
||||
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
|
||||
let is_read_only = editor.read(cx).read_only(cx);
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
color: if is_read_only {
|
||||
theme.colors().text_muted
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
theme.colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
@@ -195,15 +197,15 @@ impl Console {
|
||||
..Default::default()
|
||||
};
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(&self.query_bar, self.editor_style(cx))
|
||||
EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
|
||||
}
|
||||
|
||||
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -250,9 +250,6 @@ impl StackFrameList {
|
||||
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
if abs_path.starts_with("<node_internals>") {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
@@ -345,6 +342,7 @@ impl StackFrameList {
|
||||
s.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
.filter(|path| path.is_absolute())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ mod inline_values;
|
||||
#[cfg(test)]
|
||||
mod module_list;
|
||||
#[cfg(test)]
|
||||
mod new_session_modal;
|
||||
mod new_process_modal;
|
||||
#[cfg(test)]
|
||||
mod persistence;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use dap::DapRegistry;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Fs, Project};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
|
||||
use util::path;
|
||||
|
||||
// use crate::new_process_modal::NewProcessMode;
|
||||
use crate::tests::{init_test, init_test_workspace};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -151,105 +152,111 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
// #[gpui::test]
|
||||
// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
// init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
// let fs = FakeFs::new(executor.clone());
|
||||
// fs.insert_tree(
|
||||
// path!("/project"),
|
||||
// json!({
|
||||
// "main.rs": "fn main() {}"
|
||||
// }),
|
||||
// )
|
||||
// .await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
// let workspace = init_test_workspace(&project, cx).await;
|
||||
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
// workspace
|
||||
// .update(cx, |workspace, window, cx| {
|
||||
// crate::new_process_modal::NewProcessModal::show(
|
||||
// workspace,
|
||||
// window,
|
||||
// NewProcessMode::Debug,
|
||||
// None,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
// cx.run_until_parked();
|
||||
|
||||
let modal = workspace
|
||||
.update(cx, |workspace, _, cx| {
|
||||
workspace.active_modal::<crate::new_session_modal::NewSessionModal>(cx)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("Modal should be active");
|
||||
// let modal = workspace
|
||||
// .update(cx, |workspace, _, cx| {
|
||||
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
|
||||
// })
|
||||
// .unwrap()
|
||||
// .expect("Modal should be active");
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_custom("/project/main", "/project", false, window, cx);
|
||||
modal.save_scenario(window, cx);
|
||||
});
|
||||
// modal.update_in(cx, |modal, window, cx| {
|
||||
// modal.set_configure("/project/main", "/project", false, window, cx);
|
||||
// modal.save_scenario(window, cx);
|
||||
// });
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
// cx.executor().run_until_parked();
|
||||
|
||||
let debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist");
|
||||
// let debug_json_content = fs
|
||||
// .load(path!("/project/.zed/debug.json").as_ref())
|
||||
// .await
|
||||
// .expect("debug.json should exist");
|
||||
|
||||
let expected_content = vec![
|
||||
"[",
|
||||
" {",
|
||||
r#" "adapter": "fake-adapter","#,
|
||||
r#" "label": "main (fake-adapter)","#,
|
||||
r#" "request": "launch","#,
|
||||
r#" "program": "/project/main","#,
|
||||
r#" "cwd": "/project","#,
|
||||
r#" "args": [],"#,
|
||||
r#" "env": {}"#,
|
||||
" }",
|
||||
"]",
|
||||
];
|
||||
// let expected_content = vec![
|
||||
// "[",
|
||||
// " {",
|
||||
// r#" "adapter": "fake-adapter","#,
|
||||
// r#" "label": "main (fake-adapter)","#,
|
||||
// r#" "request": "launch","#,
|
||||
// r#" "program": "/project/main","#,
|
||||
// r#" "cwd": "/project","#,
|
||||
// r#" "args": [],"#,
|
||||
// r#" "env": {}"#,
|
||||
// " }",
|
||||
// "]",
|
||||
// ];
|
||||
|
||||
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
// pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_custom("/project/other", "/project", true, window, cx);
|
||||
modal.save_scenario(window, cx);
|
||||
});
|
||||
// modal.update_in(cx, |modal, window, cx| {
|
||||
// modal.set_configure("/project/other", "/project", true, window, cx);
|
||||
// modal.save_scenario(window, cx);
|
||||
// });
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
// cx.executor().run_until_parked();
|
||||
|
||||
let debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist after second save");
|
||||
// let debug_json_content = fs
|
||||
// .load(path!("/project/.zed/debug.json").as_ref())
|
||||
// .await
|
||||
// .expect("debug.json should exist after second save");
|
||||
|
||||
let expected_content = vec![
|
||||
"[",
|
||||
" {",
|
||||
r#" "adapter": "fake-adapter","#,
|
||||
r#" "label": "main (fake-adapter)","#,
|
||||
r#" "request": "launch","#,
|
||||
r#" "program": "/project/main","#,
|
||||
r#" "cwd": "/project","#,
|
||||
r#" "args": [],"#,
|
||||
r#" "env": {}"#,
|
||||
" },",
|
||||
" {",
|
||||
r#" "adapter": "fake-adapter","#,
|
||||
r#" "label": "other (fake-adapter)","#,
|
||||
r#" "request": "launch","#,
|
||||
r#" "program": "/project/other","#,
|
||||
r#" "cwd": "/project","#,
|
||||
r#" "args": [],"#,
|
||||
r#" "env": {}"#,
|
||||
" }",
|
||||
"]",
|
||||
];
|
||||
// let expected_content = vec![
|
||||
// "[",
|
||||
// " {",
|
||||
// r#" "adapter": "fake-adapter","#,
|
||||
// r#" "label": "main (fake-adapter)","#,
|
||||
// r#" "request": "launch","#,
|
||||
// r#" "program": "/project/main","#,
|
||||
// r#" "cwd": "/project","#,
|
||||
// r#" "args": [],"#,
|
||||
// r#" "env": {}"#,
|
||||
// " },",
|
||||
// " {",
|
||||
// r#" "adapter": "fake-adapter","#,
|
||||
// r#" "label": "other (fake-adapter)","#,
|
||||
// r#" "request": "launch","#,
|
||||
// r#" "program": "/project/other","#,
|
||||
// r#" "cwd": "/project","#,
|
||||
// r#" "args": [],"#,
|
||||
// r#" "env": {}"#,
|
||||
// " }",
|
||||
// "]",
|
||||
// ];
|
||||
|
||||
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
}
|
||||
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
// pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
// }
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
|
||||
@@ -315,7 +322,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
|
||||
);
|
||||
|
||||
let request_type = adapter
|
||||
.validate_config(&debug_scenario.config)
|
||||
.request_kind(&debug_scenario.config)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Adapter {} should validate the config successfully",
|
||||
@@ -82,6 +82,7 @@ tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
@@ -97,6 +98,7 @@ gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages = {workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
markdown = { workspace = true, features = ["test-support"] }
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -201,7 +201,7 @@ use ui::{
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc, wrap_with_prefix};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
|
||||
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
|
||||
@@ -1670,6 +1670,13 @@ impl Editor {
|
||||
editor
|
||||
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
|
||||
}
|
||||
project::Event::LanguageServerAdded(..)
|
||||
| project::Event::LanguageServerRemoved(..) => {
|
||||
if editor.tasks_update_task.is_none() {
|
||||
editor.tasks_update_task =
|
||||
Some(editor.refresh_runnables(window, cx));
|
||||
}
|
||||
}
|
||||
project::Event::SnippetEdit(id, snippet_edits) => {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
@@ -13543,6 +13550,7 @@ impl Editor {
|
||||
}
|
||||
let project = self.project.as_ref().map(Entity::downgrade);
|
||||
let task_sources = self.lsp_task_sources(cx);
|
||||
let multi_buffer = self.buffer.downgrade();
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
let Some(project) = project.and_then(|p| p.upgrade()) else {
|
||||
@@ -13626,7 +13634,19 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone());
|
||||
let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
|
||||
buffer.language_settings(cx).tasks.prefer_lsp
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let rows = Self::runnable_rows(
|
||||
project,
|
||||
display_snapshot,
|
||||
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
|
||||
new_rows,
|
||||
cx.clone(),
|
||||
);
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.clear_tasks();
|
||||
@@ -13654,15 +13674,21 @@ impl Editor {
|
||||
fn runnable_rows(
|
||||
project: Entity<Project>,
|
||||
snapshot: DisplaySnapshot,
|
||||
prefer_lsp: bool,
|
||||
runnable_ranges: Vec<RunnableRange>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
|
||||
runnable_ranges
|
||||
.into_iter()
|
||||
.filter_map(|mut runnable| {
|
||||
let tasks = cx
|
||||
let mut tasks = cx
|
||||
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
|
||||
.ok()?;
|
||||
if prefer_lsp {
|
||||
tasks.retain(|(task_kind, _)| {
|
||||
!matches!(task_kind, TaskSourceKind::Language { .. })
|
||||
});
|
||||
}
|
||||
if tasks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -19587,6 +19613,347 @@ fn update_uncommitted_diff_for_buffer(
|
||||
})
|
||||
}
|
||||
|
||||
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
|
||||
let tab_size = tab_size.get() as usize;
|
||||
let mut width = offset;
|
||||
|
||||
for ch in text.chars() {
|
||||
width += if ch == '\t' {
|
||||
tab_size - (width % tab_size)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
width - offset
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string_size_with_expanded_tabs() {
|
||||
let nz = |val| NonZeroU32::new(val).unwrap();
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
|
||||
struct WordBreakingTokenizer<'a> {
|
||||
input: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> WordBreakingTokenizer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_char_ideographic(ch: char) -> bool {
|
||||
use unicode_script::Script::*;
|
||||
use unicode_script::UnicodeScript;
|
||||
matches!(ch.script(), Han | Tangut | Yi)
|
||||
}
|
||||
|
||||
fn is_grapheme_ideographic(text: &str) -> bool {
|
||||
text.chars().any(is_char_ideographic)
|
||||
}
|
||||
|
||||
fn is_grapheme_whitespace(text: &str) -> bool {
|
||||
text.chars().any(|x| x.is_whitespace())
|
||||
}
|
||||
|
||||
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
|
||||
text.chars().next().map_or(false, |ch| {
|
||||
matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum WordBreakToken<'a> {
|
||||
Word { token: &'a str, grapheme_len: usize },
|
||||
InlineWhitespace { token: &'a str, grapheme_len: usize },
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordBreakingTokenizer<'a> {
|
||||
/// Yields a span, the count of graphemes in the token, and whether it was
|
||||
/// whitespace. Note that it also breaks at word boundaries.
|
||||
type Item = WordBreakToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iter = self.input.graphemes(true).peekable();
|
||||
let mut offset = 0;
|
||||
let mut grapheme_len = 0;
|
||||
if let Some(first_grapheme) = iter.next() {
|
||||
let is_newline = first_grapheme == "\n";
|
||||
let is_whitespace = is_grapheme_whitespace(first_grapheme);
|
||||
offset += first_grapheme.len();
|
||||
grapheme_len += 1;
|
||||
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
|
||||
if let Some(grapheme) = iter.peek().copied() {
|
||||
if should_stay_with_preceding_ideograph(grapheme) {
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut words = self.input[offset..].split_word_bound_indices().peekable();
|
||||
let mut next_word_bound = words.peek().copied();
|
||||
if next_word_bound.map_or(false, |(i, _)| i == 0) {
|
||||
next_word_bound = words.next();
|
||||
}
|
||||
while let Some(grapheme) = iter.peek().copied() {
|
||||
if next_word_bound.map_or(false, |(i, _)| i == offset) {
|
||||
break;
|
||||
};
|
||||
if is_grapheme_whitespace(grapheme) != is_whitespace
|
||||
|| (grapheme == "\n") != is_newline
|
||||
{
|
||||
break;
|
||||
};
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
let token = &self.input[..offset];
|
||||
self.input = &self.input[offset..];
|
||||
if token == "\n" {
|
||||
Some(WordBreakToken::Newline)
|
||||
} else if is_whitespace {
|
||||
Some(WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
} else {
|
||||
Some(WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_breaking_tokenizer() {
|
||||
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
|
||||
("", &[]),
|
||||
(" ", &[whitespace(" ", 2)]),
|
||||
("Ʒ", &[word("Ʒ", 1)]),
|
||||
("Ǽ", &[word("Ǽ", 1)]),
|
||||
("⋑", &[word("⋑", 1)]),
|
||||
("⋑⋑", &[word("⋑⋑", 2)]),
|
||||
(
|
||||
"原理,进而",
|
||||
&[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
|
||||
),
|
||||
(
|
||||
"hello world",
|
||||
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
"hello, world",
|
||||
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
" hello world",
|
||||
&[
|
||||
whitespace(" ", 2),
|
||||
word("hello", 5),
|
||||
whitespace(" ", 1),
|
||||
word("world", 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
"这是什么 \n 钢笔",
|
||||
&[
|
||||
word("这", 1),
|
||||
word("是", 1),
|
||||
word("什", 1),
|
||||
word("么", 1),
|
||||
whitespace(" ", 1),
|
||||
newline(),
|
||||
whitespace(" ", 1),
|
||||
word("钢", 1),
|
||||
word("笔", 1),
|
||||
],
|
||||
),
|
||||
(" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
|
||||
];
|
||||
|
||||
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn newline() -> WordBreakToken<'static> {
|
||||
WordBreakToken::Newline
|
||||
}
|
||||
|
||||
for (input, result) in tests {
|
||||
assert_eq!(
|
||||
WordBreakingTokenizer::new(input)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
*result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
match token {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
WordBreakToken::InlineWhitespace {
|
||||
mut token,
|
||||
mut grapheme_len,
|
||||
} => {
|
||||
in_whitespace = true;
|
||||
if have_preceding_whitespace && !preserve_existing_whitespace {
|
||||
continue;
|
||||
}
|
||||
if !preserve_existing_whitespace {
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
wrapped_text
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"# abcdefg"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"hello\nworld"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"// xx yy zz\n// aa bb cc"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"这是什\n么 钢\n笔"
|
||||
);
|
||||
}
|
||||
|
||||
pub trait CollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
|
||||
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;
|
||||
|
||||
@@ -9111,11 +9111,10 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
assert_eq!(params.options.tab_size, 8);
|
||||
Ok(Some(vec![]))
|
||||
Ok(Some(Vec::new()))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
}
|
||||
|
||||
@@ -16769,9 +16768,9 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
|
||||
async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16784,10 +16783,10 @@ async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16800,14 +16799,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_nested(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16829,11 +16828,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_tab(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16963,6 +16962,72 @@ async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
if a {
|
||||
b(
|
||||
c,
|
||||
d,
|
||||
)
|
||||
} else {
|
||||
e(
|
||||
f
|
||||
)
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 10, 0),
|
||||
indent_guide(buffer_id, 2, 5, 1),
|
||||
indent_guide(buffer_id, 7, 9, 1),
|
||||
indent_guide(buffer_id, 3, 4, 2),
|
||||
indent_guide(buffer_id, 8, 8, 2),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.fold_at(MultiBufferRow(2), window, cx);
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"
|
||||
fn main() {
|
||||
if a {
|
||||
b(⋯
|
||||
)
|
||||
} else {
|
||||
e(
|
||||
f
|
||||
)
|
||||
}
|
||||
}"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 10, 0),
|
||||
indent_guide(buffer_id, 2, 5, 1),
|
||||
indent_guide(buffer_id, 7, 9, 1),
|
||||
indent_guide(buffer_id, 8, 8, 2),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
|
||||
@@ -7607,7 +7607,10 @@ impl Element for EditorElement {
|
||||
editor.gutter_dimensions = gutter_dimensions;
|
||||
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
|
||||
|
||||
if matches!(editor.mode, EditorMode::Minimap { .. }) {
|
||||
if matches!(
|
||||
editor.mode,
|
||||
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
snapshot
|
||||
} else {
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
@@ -9626,7 +9629,6 @@ fn compute_auto_height_layout(
|
||||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||||
let line_height = style.text.line_height_in_pixels(window.rem_size());
|
||||
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
|
||||
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
|
||||
|
||||
let mut snapshot = editor.snapshot(window, cx);
|
||||
let gutter_dimensions = snapshot
|
||||
@@ -9643,18 +9645,10 @@ fn compute_auto_height_layout(
|
||||
let overscroll = size(em_width, px(0.));
|
||||
|
||||
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||||
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
|
||||
let editor_content_width = editor_width - content_offset.x;
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
let wrap_width = match editor.soft_wrap_mode(cx) {
|
||||
SoftWrap::GitDiff => None,
|
||||
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
|
||||
SoftWrap::EditorWidth => Some(editor_content_width),
|
||||
SoftWrap::Column(column) => Some(wrap_width_for(column)),
|
||||
SoftWrap::Bounded(column) => Some(editor_content_width.min(wrap_width_for(column))),
|
||||
};
|
||||
if editor.set_wrap_width(wrap_width, cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) {
|
||||
if editor.set_wrap_width(Some(editor_width), cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||||
|
||||
@@ -1050,7 +1050,9 @@ mod tests {
|
||||
|
||||
for (range, event) in slice.iter() {
|
||||
match event {
|
||||
MarkdownEvent::SubstitutedText(parsed) => rendered_text.push_str(parsed),
|
||||
MarkdownEvent::SubstitutedText(parsed) => {
|
||||
rendered_text.push_str(parsed.as_str())
|
||||
}
|
||||
MarkdownEvent::Text | MarkdownEvent::Code => {
|
||||
rendered_text.push_str(&text[range.clone()])
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
use std::{cmp::Ordering, ops::Range, time::Duration};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{App, AppContext as _, Context, Task, Window};
|
||||
use language::language_settings::language_settings;
|
||||
use multi_buffer::{IndentGuide, MultiBufferRow};
|
||||
use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
|
||||
use text::{LineIndent, Point};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<IndentGuide> {
|
||||
let start_anchor = snapshot
|
||||
let start_offset = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
|
||||
let end_anchor = snapshot
|
||||
.point_to_offset(Point::new(visible_buffer_range.start.0, 0));
|
||||
let end_offset = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
|
||||
.point_to_offset(Point::new(visible_buffer_range.end.0, 0));
|
||||
let start_anchor = snapshot.buffer_snapshot.anchor_before(start_offset);
|
||||
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
|
||||
|
||||
let mut fold_ranges = Vec::<Range<Point>>::new();
|
||||
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
|
||||
while let Some(fold) = folds.next() {
|
||||
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
|
||||
if let Some(last_range) = fold_ranges.last_mut() {
|
||||
if last_range.end >= start {
|
||||
last_range.end = last_range.end.max(end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
fold_ranges.push(start..end);
|
||||
}
|
||||
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
@@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
|
||||
return false;
|
||||
}
|
||||
|
||||
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
|
||||
// Filter out indent guides that are inside a fold
|
||||
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
|
||||
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
|
||||
let is_folded = snapshot.is_line_folded(start);
|
||||
let line_indent = snapshot.line_indent_for_buffer_row(start);
|
||||
let contained_in_fold =
|
||||
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
|
||||
!(is_folded && contained_in_fold)
|
||||
let has_containing_fold = fold_ranges
|
||||
.binary_search_by(|fold_range| {
|
||||
if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
|
||||
Ordering::Greater
|
||||
} else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
!has_containing_fold
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::Editor;
|
||||
use collections::HashMap;
|
||||
@@ -16,10 +17,12 @@ use project::LocationLink;
|
||||
use project::Project;
|
||||
use project::TaskSourceKind;
|
||||
use project::lsp_store::lsp_ext_command::GetLspRunnables;
|
||||
use smol::future::FutureExt as _;
|
||||
use smol::stream::StreamExt;
|
||||
use task::ResolvedTask;
|
||||
use task::TaskContext;
|
||||
use text::BufferId;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub(crate) fn find_specific_language_server_in_selection<F>(
|
||||
@@ -130,44 +133,70 @@ pub fn lsp_tasks(
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut lsp_tasks = Vec::new();
|
||||
while let Some(server_to_query) = lsp_task_sources.next().await {
|
||||
if let Some((server_id, buffers)) = server_to_query {
|
||||
let source_kind = TaskSourceKind::Lsp(server_id);
|
||||
let id_base = source_kind.to_id_base();
|
||||
let mut new_lsp_tasks = Vec::new();
|
||||
for buffer in buffers {
|
||||
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(runnables_task) = project.update(cx, |project, cx| {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetLspRunnables {
|
||||
buffer_id,
|
||||
position: for_position,
|
||||
cx.spawn(async move |cx| {
|
||||
let mut lsp_tasks = HashMap::default();
|
||||
while let Some(server_to_query) = lsp_task_sources.next().await {
|
||||
if let Some((server_id, buffers)) = server_to_query {
|
||||
let mut new_lsp_tasks = Vec::new();
|
||||
for buffer in buffers {
|
||||
let source_kind = match buffer.update(cx, |buffer, _| {
|
||||
buffer.language().map(|language| language.name())
|
||||
}) {
|
||||
Ok(Some(language_name)) => TaskSourceKind::Lsp {
|
||||
server: server_id,
|
||||
language_name: SharedString::from(language_name),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}) {
|
||||
if let Some(new_runnables) = runnables_task.await.log_err() {
|
||||
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|
||||
|(location, runnable)| {
|
||||
let resolved_task =
|
||||
runnable.resolve_task(&id_base, &lsp_buffer_context)?;
|
||||
Some((location, resolved_task))
|
||||
Ok(None) => continue,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
let id_base = source_kind.to_id_base();
|
||||
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(runnables_task) = project.update(cx, |project, cx| {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetLspRunnables {
|
||||
buffer_id,
|
||||
position: for_position,
|
||||
},
|
||||
));
|
||||
cx,
|
||||
)
|
||||
}) {
|
||||
if let Some(new_runnables) = runnables_task.await.log_err() {
|
||||
new_lsp_tasks.extend(
|
||||
new_runnables.runnables.into_iter().filter_map(
|
||||
|(location, runnable)| {
|
||||
let resolved_task = runnable
|
||||
.resolve_task(&id_base, &lsp_buffer_context)?;
|
||||
Some((location, resolved_task))
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
lsp_tasks
|
||||
.entry(source_kind)
|
||||
.or_insert_with(Vec::new)
|
||||
.append(&mut new_lsp_tasks);
|
||||
}
|
||||
}
|
||||
lsp_tasks.push((source_kind, new_lsp_tasks));
|
||||
}
|
||||
}
|
||||
lsp_tasks
|
||||
lsp_tasks.into_iter().collect()
|
||||
})
|
||||
.race({
|
||||
// `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast
|
||||
let timer = cx.background_executor().timer(Duration::from_millis(200));
|
||||
async move {
|
||||
timer.await;
|
||||
log::info!("Timed out waiting for LSP tasks");
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -67,3 +67,4 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
@@ -19,6 +19,7 @@ use collections::HashMap;
|
||||
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity};
|
||||
use language_model::{LanguageModel, Role, StopReason};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
|
||||
@@ -307,7 +308,7 @@ impl ExampleContext {
|
||||
|
||||
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
|
||||
thread.set_remaining_turns(iterations);
|
||||
thread.send_to_model(model, None, cx);
|
||||
thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
|
||||
thread.messages().len()
|
||||
})?;
|
||||
|
||||
|
||||
@@ -576,6 +576,7 @@ impl ExampleInstance {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(to_prompt(assertion.description))],
|
||||
|
||||
@@ -1527,7 +1527,6 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("open-selection", "Open").on_click(|_, window, cx| {
|
||||
|
||||
@@ -59,6 +59,7 @@ util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -13,7 +13,6 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
|
||||
use editor::{
|
||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
scroll::ScrollbarAutoHide,
|
||||
@@ -42,6 +41,7 @@ use language_model::{
|
||||
};
|
||||
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use multi_buffer::ExcerptInfo;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use panel::{
|
||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
@@ -54,7 +54,6 @@ use project::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::future::Future;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
@@ -63,15 +62,14 @@ use ui::{
|
||||
Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe, wrap_with_prefix};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::AppState;
|
||||
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::DetachAndPromptErr,
|
||||
};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
actions!(
|
||||
git_panel,
|
||||
@@ -385,6 +383,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
commit_editor.set_hard_wrap(Some(72), cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
commit_editor.set_placeholder_text(placeholder, cx);
|
||||
commit_editor
|
||||
@@ -1486,22 +1485,8 @@ impl GitPanel {
|
||||
|
||||
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
|
||||
let message = self.commit_editor.read(cx).text(cx);
|
||||
let width = self
|
||||
.commit_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.language_settings(cx)
|
||||
.preferred_line_length as usize;
|
||||
|
||||
if !message.trim().is_empty() {
|
||||
let message = wrap_with_prefix(
|
||||
String::new(),
|
||||
message,
|
||||
width,
|
||||
NonZeroU32::new(8).unwrap(), // tab size doesn't matter when prefix is empty
|
||||
false,
|
||||
);
|
||||
return Some(message);
|
||||
}
|
||||
|
||||
@@ -1781,6 +1766,7 @@ impl GitPanel {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::GenerateGitCommitMessage),
|
||||
mode: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
|
||||
@@ -202,6 +202,7 @@ pub enum Part {
|
||||
InlineDataPart(InlineDataPart),
|
||||
FunctionCallPart(FunctionCallPart),
|
||||
FunctionResponsePart(FunctionResponsePart),
|
||||
ThoughtPart(ThoughtPart),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -235,6 +236,13 @@ pub struct FunctionResponsePart {
|
||||
pub function_response: FunctionResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThoughtPart {
|
||||
pub thought: bool,
|
||||
pub thought_signature: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CitationSource {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, KeyBinding, SharedString, Timer, Window, WindowBounds,
|
||||
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, SharedString, Timer,
|
||||
Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
struct SubWindow {
|
||||
@@ -169,6 +169,42 @@ impl Render for WindowDemo {
|
||||
let content_size = window.bounds().size;
|
||||
window.resize(size(content_size.height, content_size.width));
|
||||
}))
|
||||
.child(button("Prompt", |window, cx| {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure?",
|
||||
None,
|
||||
&["Ok", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
if answer.await.unwrap() == 0 {
|
||||
println!("You have clicked Ok");
|
||||
} else {
|
||||
println!("You have clicked Cancel");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.child(button("Prompt (non-English)", |window, cx| {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure?",
|
||||
None,
|
||||
&[PromptButton::ok("确定"), PromptButton::cancel("取消")],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
if answer.await.unwrap() == 0 {
|
||||
println!("You have clicked Ok");
|
||||
} else {
|
||||
println!("You have clicked Cancel");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +231,7 @@ fn main() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
|
||||
@@ -37,10 +37,10 @@ use crate::{
|
||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
|
||||
Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
|
||||
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
@@ -1578,14 +1578,14 @@ impl App {
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
&[PromptButton],
|
||||
PromptHandle,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> RenderablePromptHandle
|
||||
+ 'static,
|
||||
) {
|
||||
self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)))
|
||||
self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)));
|
||||
}
|
||||
|
||||
/// Reset the prompt builder to the default implementation.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptLevel, Render, Reservation,
|
||||
Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
|
||||
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -314,13 +314,16 @@ impl AsyncWindowContext {
|
||||
/// Present a platform dialog.
|
||||
/// The provided message will be presented, along with buttons for each answer.
|
||||
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||
pub fn prompt(
|
||||
pub fn prompt<T>(
|
||||
&mut self,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
answers: &[T],
|
||||
) -> oneshot::Receiver<usize>
|
||||
where
|
||||
T: Clone + Into<PromptButton>,
|
||||
{
|
||||
self.window
|
||||
.update(self, |_, window, cx| {
|
||||
window.prompt(level, message, detail, answers, cx)
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
///
|
||||
/// The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||
/// (Though mostly when developing Zed itself, you just need to add a new line to
|
||||
/// assets/keymaps/default.json).
|
||||
/// assets/keymaps/default-{platform}.json).
|
||||
///
|
||||
/// ```rust
|
||||
/// cx.bind_keys([
|
||||
|
||||
@@ -418,7 +418,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
) -> Option<oneshot::Receiver<usize>>;
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
@@ -1244,6 +1244,58 @@ pub enum PromptLevel {
|
||||
Critical,
|
||||
}
|
||||
|
||||
/// Prompt Button
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PromptButton {
|
||||
/// Ok button
|
||||
Ok(SharedString),
|
||||
/// Cancel button
|
||||
Cancel(SharedString),
|
||||
/// Other button
|
||||
Other(SharedString),
|
||||
}
|
||||
|
||||
impl PromptButton {
|
||||
/// Create a button with label
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
PromptButton::Other(label.into())
|
||||
}
|
||||
|
||||
/// Create an Ok button
|
||||
pub fn ok(label: impl Into<SharedString>) -> Self {
|
||||
PromptButton::Ok(label.into())
|
||||
}
|
||||
|
||||
/// Create a Cancel button
|
||||
pub fn cancel(label: impl Into<SharedString>) -> Self {
|
||||
PromptButton::Cancel(label.into())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_cancel(&self) -> bool {
|
||||
matches!(self, PromptButton::Cancel(_))
|
||||
}
|
||||
|
||||
/// Returns the label of the button
|
||||
pub fn label(&self) -> &SharedString {
|
||||
match self {
|
||||
PromptButton::Ok(label) => label,
|
||||
PromptButton::Cancel(label) => label,
|
||||
PromptButton::Other(label) => label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for PromptButton {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().as_str() {
|
||||
"ok" => PromptButton::Ok("Ok".into()),
|
||||
"cancel" => PromptButton::Cancel("Cancel".into()),
|
||||
_ => PromptButton::Other(SharedString::from(value.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of the cursor (pointer)
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum CursorStyle {
|
||||
|
||||
@@ -29,8 +29,8 @@ use crate::platform::{
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, RequestFrameOptions, ResizeEdge,
|
||||
ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
|
||||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowParams, px,
|
||||
size,
|
||||
};
|
||||
@@ -862,7 +862,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
_answers: &[PromptButton],
|
||||
) -> Option<Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, Tiling,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowParams, X11ClientStatePtr, px, size,
|
||||
Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
|
||||
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
WindowKind, WindowParams, X11ClientStatePtr, px, size,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
@@ -1227,7 +1227,7 @@ impl PlatformWindow for X11Window {
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
_answers: &[PromptButton],
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const SPACE_KEY: u16 = b' ' as u16;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const NUMPAD_ENTER_KEY: u16 = 0x03;
|
||||
const ESCAPE_KEY: u16 = 0x1b;
|
||||
pub(crate) const ESCAPE_KEY: u16 = 0x1b;
|
||||
const TAB_KEY: u16 = 0x09;
|
||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::{
|
||||
AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
|
||||
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
|
||||
PlatformWindow, Point, PromptLevel, RequestFrameOptions, ScaledPixels, Size, Timer,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
|
||||
PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size,
|
||||
Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
|
||||
platform::PlatformInputHandler, point, px, size,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
@@ -902,7 +902,7 @@ impl PlatformWindow for MacWindow {
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
) -> Option<oneshot::Receiver<usize>> {
|
||||
// macOs applies overrides to modal window buttons after they are added.
|
||||
// Two most important for this logic are:
|
||||
@@ -926,7 +926,7 @@ impl PlatformWindow for MacWindow {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, label)| **label != "Cancel")
|
||||
.find(|(_, label)| !label.is_cancel())
|
||||
.filter(|&(label_index, _)| label_index > 0);
|
||||
|
||||
unsafe {
|
||||
@@ -948,11 +948,19 @@ impl PlatformWindow for MacWindow {
|
||||
.enumerate()
|
||||
.filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
|
||||
{
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
|
||||
if answer.is_cancel() {
|
||||
// Bind Escape Key to Cancel Button
|
||||
if let Some(key) = std::char::from_u32(super::events::ESCAPE_KEY as u32) {
|
||||
let _: () =
|
||||
msg_send![button, setKeyEquivalent: ns_string(&key.to_string())];
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
@@ -165,10 +165,10 @@ impl TestPlatform {
|
||||
&self,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let answers: Vec<String> = answers.iter().map(|&s| s.to_string()).collect();
|
||||
let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
|
||||
self.background_executor()
|
||||
.set_waiting_hint(Some(format!("PROMPT: {:?} {:?}", msg, detail)));
|
||||
self.prompts
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -164,7 +164,7 @@ impl PlatformWindow for TestWindow {
|
||||
_level: crate::PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
Some(
|
||||
self.0
|
||||
|
||||
@@ -608,7 +608,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
) -> Option<Receiver<usize>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let msg = msg.to_string();
|
||||
@@ -616,8 +616,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
Some(info) => Some(info.to_string()),
|
||||
None => None,
|
||||
};
|
||||
let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let handle = self.0.hwnd;
|
||||
let answers = answers.to_vec();
|
||||
self.0
|
||||
.executor
|
||||
.spawn(async move {
|
||||
@@ -653,9 +653,9 @@ impl PlatformWindow for WindowsWindow {
|
||||
let mut button_id_map = Vec::with_capacity(answers.len());
|
||||
let mut buttons = Vec::new();
|
||||
let mut btn_encoded = Vec::new();
|
||||
for (index, btn_string) in answers.iter().enumerate() {
|
||||
let encoded = HSTRING::from(btn_string);
|
||||
let button_id = if btn_string == "Cancel" {
|
||||
for (index, btn) in answers.iter().enumerate() {
|
||||
let encoded = HSTRING::from(btn.label().as_ref());
|
||||
let button_id = if btn.is_cancel() {
|
||||
IDCANCEL.0
|
||||
} else {
|
||||
index as i32 - 100
|
||||
|
||||
@@ -9,13 +9,13 @@ use crate::{
|
||||
KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers,
|
||||
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
|
||||
RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
|
||||
SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
|
||||
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
|
||||
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
|
||||
point, prelude::*, px, rems, size, transparent_black,
|
||||
PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render,
|
||||
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
|
||||
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
|
||||
StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
|
||||
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
|
||||
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
@@ -3705,28 +3705,36 @@ impl Window {
|
||||
/// Present a platform dialog.
|
||||
/// The provided message will be presented, along with buttons for each answer.
|
||||
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||
pub fn prompt(
|
||||
pub fn prompt<T>(
|
||||
&mut self,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[T],
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<usize> {
|
||||
) -> oneshot::Receiver<usize>
|
||||
where
|
||||
T: Clone + Into<PromptButton>,
|
||||
{
|
||||
let prompt_builder = cx.prompt_builder.take();
|
||||
let Some(prompt_builder) = prompt_builder else {
|
||||
unreachable!("Re-entrant window prompting is not supported by GPUI");
|
||||
};
|
||||
|
||||
let answers = answers
|
||||
.iter()
|
||||
.map(|answer| answer.clone().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let receiver = match &prompt_builder {
|
||||
PromptBuilder::Default => self
|
||||
.platform_window
|
||||
.prompt(level, message, detail, answers)
|
||||
.prompt(level, message, detail, &answers)
|
||||
.unwrap_or_else(|| {
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
|
||||
}),
|
||||
PromptBuilder::Custom(_) => {
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3741,7 +3749,7 @@ impl Window {
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
answers: &[PromptButton],
|
||||
cx: &mut App,
|
||||
) -> oneshot::Receiver<usize> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::channel::oneshot;
|
||||
|
||||
use crate::{
|
||||
AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, PromptLevel, Render,
|
||||
InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
|
||||
StatefulInteractiveElement, Styled, div, opaque_grey, white,
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn fallback_prompt_renderer(
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
actions: &[&str],
|
||||
actions: &[PromptButton],
|
||||
handle: PromptHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -83,7 +83,7 @@ pub fn fallback_prompt_renderer(
|
||||
_level: level,
|
||||
message: message.to_string(),
|
||||
detail: detail.map(ToString::to_string),
|
||||
actions: actions.iter().map(ToString::to_string).collect(),
|
||||
actions: actions.to_vec(),
|
||||
focus: cx.focus_handle(),
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ pub struct FallbackPromptRenderer {
|
||||
_level: PromptLevel,
|
||||
message: String,
|
||||
detail: Option<String>,
|
||||
actions: Vec<String>,
|
||||
actions: Vec<PromptButton>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Render for FallbackPromptRenderer {
|
||||
.rounded_xs()
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.child(action.clone())
|
||||
.child(action.label().clone())
|
||||
.id(ix)
|
||||
.on_click(cx.listener(move |_, _, _, cx| {
|
||||
cx.emit(PromptResponse(ix));
|
||||
@@ -202,7 +202,7 @@ pub(crate) enum PromptBuilder {
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
&[PromptButton],
|
||||
PromptHandle,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
@@ -216,7 +216,7 @@ impl Deref for PromptBuilder {
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
&[PromptButton],
|
||||
PromptHandle,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
|
||||
@@ -179,6 +179,8 @@ pub enum IconName {
|
||||
PhoneIncoming,
|
||||
Pin,
|
||||
Play,
|
||||
PlayAlt,
|
||||
PlayBug,
|
||||
Plus,
|
||||
PocketKnife,
|
||||
Power,
|
||||
|
||||
@@ -64,7 +64,7 @@ use std::{
|
||||
use std::{num::NonZeroU32, sync::OnceLock};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{ContextProvider, RunnableRange};
|
||||
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
|
||||
pub use text_diff::{
|
||||
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
|
||||
};
|
||||
@@ -680,7 +680,7 @@ pub struct CodeLabel {
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct LanguageConfig {
|
||||
/// Human-readable name of the language.
|
||||
pub name: LanguageName,
|
||||
@@ -791,7 +791,7 @@ pub struct LanguageMatcher {
|
||||
}
|
||||
|
||||
/// The configuration for JSX tag auto-closing.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct JsxTagAutoCloseConfig {
|
||||
/// The name of the node for a opening tag
|
||||
pub open_tag_node_name: String,
|
||||
@@ -824,7 +824,7 @@ pub struct JsxTagAutoCloseConfig {
|
||||
}
|
||||
|
||||
/// The configuration for documentation block for this language.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct DocumentationConfig {
|
||||
/// A start tag of documentation block.
|
||||
pub start: Arc<str>,
|
||||
|
||||
@@ -1045,6 +1045,15 @@ pub struct LanguageTaskConfig {
|
||||
pub variables: HashMap<String, String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Use LSP tasks over Zed language extension ones.
|
||||
/// If no LSP tasks are returned due to error/timeout or regular execution,
|
||||
/// Zed language extension tasks will be used instead.
|
||||
///
|
||||
/// Other Zed tasks will still be shown:
|
||||
/// * Zed task from either of the task config file
|
||||
/// * Zed task from history (e.g. one-off task was spawned before)
|
||||
#[serde(default = "default_true")]
|
||||
pub prefer_lsp: bool,
|
||||
}
|
||||
|
||||
impl InlayHintSettings {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{LanguageToolchainStore, Location, Runnable};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Task};
|
||||
use lsp::LanguageServerName;
|
||||
use task::{TaskTemplates, TaskVariables};
|
||||
@@ -26,11 +27,12 @@ pub trait ContextProvider: Send + Sync {
|
||||
fn build_context(
|
||||
&self,
|
||||
_variables: &TaskVariables,
|
||||
_location: &Location,
|
||||
_location: ContextLocation<'_>,
|
||||
_project_env: Option<HashMap<String, String>>,
|
||||
_toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<TaskVariables>> {
|
||||
let _ = _location;
|
||||
Task::ready(Ok(TaskVariables::default()))
|
||||
}
|
||||
|
||||
@@ -48,3 +50,10 @@ pub trait ContextProvider: Send + Sync {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about the place in the project we gather the context for.
|
||||
pub struct ContextLocation<'a> {
|
||||
pub fs: Option<Arc<dyn Fs>>,
|
||||
pub worktree_root: Option<PathBuf>,
|
||||
pub file_location: &'a Location,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
use image::codecs::png::PngEncoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
use zed_llm_client::CompletionMode;
|
||||
use zed_llm_client::{CompletionIntent, CompletionMode};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub struct LanguageModelImage {
|
||||
@@ -281,6 +281,7 @@ pub enum LanguageModelToolChoice {
|
||||
pub struct LanguageModelRequest {
|
||||
pub thread_id: Option<String>,
|
||||
pub prompt_id: Option<String>,
|
||||
pub intent: Option<CompletionIntent>,
|
||||
pub mode: Option<CompletionMode>,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub tools: Vec<LanguageModelRequestTool>,
|
||||
|
||||
@@ -809,6 +809,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
> {
|
||||
let thread_id = request.thread_id.clone();
|
||||
let prompt_id = request.prompt_id.clone();
|
||||
let intent = request.intent;
|
||||
let mode = request.mode;
|
||||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
match self.model.provider {
|
||||
@@ -841,6 +842,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
@@ -897,6 +899,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
@@ -934,6 +937,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.model_id.clone(),
|
||||
|
||||
@@ -623,6 +623,7 @@ impl GoogleEventMapper {
|
||||
)));
|
||||
}
|
||||
Part::FunctionResponsePart(_) => {}
|
||||
Part::ThoughtPart(_) => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,6 +820,7 @@ mod tests {
|
||||
tool_choice: None,
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
stop: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -864,6 +864,7 @@ mod tests {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
|
||||
@@ -38,6 +38,7 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -87,6 +88,7 @@ tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-yaml = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -49,6 +49,14 @@ mod tests {
|
||||
assert_eq!(buffer.text(), expected);
|
||||
};
|
||||
|
||||
// Do not indent after shebang
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
cx,
|
||||
"#!/usr/bin/env bash\n#",
|
||||
"#!/usr/bin/env bash\n#",
|
||||
);
|
||||
|
||||
// indent function correctly
|
||||
expect_indents_to(
|
||||
&mut buffer,
|
||||
|
||||
@@ -29,6 +29,6 @@ brackets = [
|
||||
### bar
|
||||
### fi
|
||||
### ```
|
||||
increase_indent_pattern = "(\\s*|;)(do|then|in|else|elif)\\b.*$"
|
||||
decrease_indent_pattern = "(\\s*|;)\\b(fi|done|esac|else|elif)\\b.*$"
|
||||
increase_indent_pattern = "(^|\\s+|;)(do|then|in|else|elif)\\b.*$"
|
||||
decrease_indent_pattern = "(^|\\s+|;)(fi|done|esac|else|elif)\\b.*$"
|
||||
# make sure to test each line mode & block mode
|
||||
|
||||
@@ -444,12 +444,13 @@ impl ContextProvider for GoContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
variables: &TaskVariables,
|
||||
location: &Location,
|
||||
location: ContextLocation<'_>,
|
||||
_: Option<HashMap<String, String>>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut gpui::App,
|
||||
) -> Task<Result<TaskVariables>> {
|
||||
let local_abs_path = location
|
||||
.file_location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
|
||||
@@ -88,7 +88,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
||||
let rust_context_provider = Arc::new(rust::RustContextProvider);
|
||||
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
|
||||
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
|
||||
let typescript_context = Arc::new(typescript::typescript_task_context());
|
||||
let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new());
|
||||
let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone()));
|
||||
let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone()));
|
||||
let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone()));
|
||||
|
||||
@@ -4,11 +4,11 @@ use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Task};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageToolchainStore;
|
||||
use language::Toolchain;
|
||||
use language::ToolchainList;
|
||||
use language::ToolchainLister;
|
||||
use language::language_settings::language_settings;
|
||||
use language::{ContextLocation, LanguageToolchainStore};
|
||||
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
|
||||
use lsp::LanguageServerBinary;
|
||||
@@ -357,6 +357,9 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
|
||||
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
|
||||
|
||||
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
|
||||
|
||||
@@ -364,35 +367,45 @@ impl ContextProvider for PythonContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
variables: &task::TaskVariables,
|
||||
location: &project::Location,
|
||||
location: ContextLocation<'_>,
|
||||
_: Option<HashMap<String, String>>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut gpui::App,
|
||||
) -> Task<Result<task::TaskVariables>> {
|
||||
let test_target = match selected_test_runner(location.buffer.read(cx).file(), cx) {
|
||||
TestRunner::UNITTEST => self.build_unittest_target(variables),
|
||||
TestRunner::PYTEST => self.build_pytest_target(variables),
|
||||
};
|
||||
let test_target =
|
||||
match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) {
|
||||
TestRunner::UNITTEST => self.build_unittest_target(variables),
|
||||
TestRunner::PYTEST => self.build_pytest_target(variables),
|
||||
};
|
||||
|
||||
let module_target = self.build_module_target(variables);
|
||||
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
|
||||
let worktree_id = location
|
||||
.file_location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|f| f.worktree_id(cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let active_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
let raw_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
toolchains
|
||||
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
|
||||
.await
|
||||
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
|
||||
.map_or_else(
|
||||
|| String::from("python3"),
|
||||
|toolchain| toolchain.path.to_string(),
|
||||
)
|
||||
} else {
|
||||
String::from("python3")
|
||||
};
|
||||
let active_toolchain = format!("\"{raw_toolchain}\"");
|
||||
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
|
||||
|
||||
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
|
||||
Ok(task::TaskVariables::from_iter(
|
||||
test_target
|
||||
.into_iter()
|
||||
.chain(module_target.into_iter())
|
||||
.chain([toolchain]),
|
||||
.chain([toolchain, raw_toolchain]),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -557,12 +557,13 @@ impl ContextProvider for RustContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
task_variables: &TaskVariables,
|
||||
location: &Location,
|
||||
location: ContextLocation<'_>,
|
||||
project_env: Option<HashMap<String, String>>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut gpui::App,
|
||||
) -> Task<Result<TaskVariables>> {
|
||||
let local_abs_path = location
|
||||
.file_location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
|
||||
@@ -2,56 +2,416 @@ use anyhow::{Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use gpui::AsyncApp;
|
||||
use gpui::{App, AppContext, AsyncApp, Task};
|
||||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
|
||||
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
|
||||
use language::{
|
||||
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
use serde_json::{Value, json};
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||||
use util::archive::extract_zip;
|
||||
use util::merge_json_value_into;
|
||||
use util::{ResultExt, fs::remove_matching, maybe};
|
||||
|
||||
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "jest file test".to_owned(),
|
||||
command: "npx jest".to_owned(),
|
||||
args: vec![VariableName::File.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "jest test $ZED_SYMBOL".to_owned(),
|
||||
command: "npx jest".to_owned(),
|
||||
pub(crate) struct TypeScriptContextProvider {
|
||||
last_package_json: PackageJsonContents,
|
||||
}
|
||||
|
||||
const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
|
||||
const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
|
||||
const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
|
||||
|
||||
const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
|
||||
const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
|
||||
const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUILD_SCRIPT"));
|
||||
const TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_TEST_SCRIPT"));
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
|
||||
|
||||
struct PackageJson {
|
||||
mtime: DateTime<Local>,
|
||||
data: PackageJsonData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct PackageJsonData {
|
||||
jest: bool,
|
||||
mocha: bool,
|
||||
vitest: bool,
|
||||
jasmine: bool,
|
||||
build_script: bool,
|
||||
test_script: bool,
|
||||
runner: Runner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum Runner {
|
||||
#[default]
|
||||
Npm,
|
||||
Npx,
|
||||
Pnpm,
|
||||
}
|
||||
|
||||
impl PackageJsonData {
|
||||
fn new(package_json: HashMap<String, Value>) -> Self {
|
||||
let mut build_script = false;
|
||||
let mut test_script = false;
|
||||
if let Some(serde_json::Value::Object(scripts)) = package_json.get("scripts") {
|
||||
build_script |= scripts.contains_key("build");
|
||||
test_script |= scripts.contains_key("test");
|
||||
}
|
||||
|
||||
let mut jest = false;
|
||||
let mut mocha = false;
|
||||
let mut vitest = false;
|
||||
let mut jasmine = false;
|
||||
if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
|
||||
jest |= dependencies.contains_key("jest");
|
||||
mocha |= dependencies.contains_key("mocha");
|
||||
vitest |= dependencies.contains_key("vitest");
|
||||
jasmine |= dependencies.contains_key("jasmine");
|
||||
}
|
||||
if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
|
||||
{
|
||||
jest |= dev_dependencies.contains_key("jest");
|
||||
mocha |= dev_dependencies.contains_key("mocha");
|
||||
vitest |= dev_dependencies.contains_key("vitest");
|
||||
jasmine |= dev_dependencies.contains_key("jasmine");
|
||||
}
|
||||
|
||||
let mut runner = Runner::Npm;
|
||||
if which::which("pnpm").is_ok() {
|
||||
runner = Runner::Pnpm;
|
||||
} else if which::which("npx").is_ok() {
|
||||
runner = Runner::Npx;
|
||||
}
|
||||
|
||||
Self {
|
||||
jest,
|
||||
mocha,
|
||||
vitest,
|
||||
jasmine,
|
||||
build_script,
|
||||
test_script,
|
||||
runner,
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_variables(&self, variables: &mut TaskVariables) {
|
||||
let runner = match self.runner {
|
||||
Runner::Npm => "npm",
|
||||
Runner::Npx => "npx",
|
||||
Runner::Pnpm => "pnpm",
|
||||
};
|
||||
variables.insert(TYPESCRIPT_RUNNER_VARIABLE, runner.to_owned());
|
||||
|
||||
if self.jest {
|
||||
variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
|
||||
}
|
||||
if self.mocha {
|
||||
variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
|
||||
}
|
||||
if self.vitest {
|
||||
variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());
|
||||
}
|
||||
if self.jasmine {
|
||||
variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());
|
||||
}
|
||||
if self.build_script {
|
||||
variables.insert(TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE, "build".to_owned());
|
||||
}
|
||||
if self.test_script {
|
||||
variables.insert(TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE, "test".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeScriptContextProvider {
|
||||
pub fn new() -> Self {
|
||||
TypeScriptContextProvider {
|
||||
last_package_json: PackageJsonContents::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for TypeScriptContextProvider {
|
||||
fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {
|
||||
let mut task_templates = TaskTemplates(Vec::new());
|
||||
|
||||
// Jest tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"--testNamePattern".into(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::File.template_value(),
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec!["ts-test".into(), "js-test".into(), "tsx-test".into()],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "execute selection $ZED_SELECTED_TEXT".to_owned(),
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Vitest tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
"run".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
"run".to_owned(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Mocha tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
"--grep".to_owned(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Jasmine tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
format!("--filter={}", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
for package_json_script in [
|
||||
TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE,
|
||||
TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE,
|
||||
] {
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"package.json script {}",
|
||||
package_json_script.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"--prefix".to_owned(),
|
||||
VariableName::WorktreeRoot.template_value(),
|
||||
"run".to_owned(),
|
||||
package_json_script.template_value(),
|
||||
],
|
||||
tags: vec!["package-script".into()],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"execute selection {}",
|
||||
VariableName::SelectedText.template_value()
|
||||
),
|
||||
command: "node".to_owned(),
|
||||
args: vec![
|
||||
"-e".into(),
|
||||
"-e".to_owned(),
|
||||
format!("\"{}\"", VariableName::SelectedText.template_value()),
|
||||
],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
});
|
||||
|
||||
Some(task_templates)
|
||||
}
|
||||
|
||||
fn build_context(
|
||||
&self,
|
||||
_variables: &task::TaskVariables,
|
||||
location: ContextLocation<'_>,
|
||||
_project_env: Option<HashMap<String, String>>,
|
||||
_toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<task::TaskVariables>> {
|
||||
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
|
||||
return Task::ready(Ok(task::TaskVariables::default()));
|
||||
};
|
||||
|
||||
let package_json_contents = self.last_package_json.clone();
|
||||
cx.background_spawn(async move {
|
||||
let variables = package_json_variables(fs, worktree_root, package_json_contents)
|
||||
.await
|
||||
.context("package.json context retrieval")
|
||||
.log_err()
|
||||
.unwrap_or_else(task::TaskVariables::default);
|
||||
Ok(variables)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn package_json_variables(
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree_root: PathBuf,
|
||||
package_json_contents: PackageJsonContents,
|
||||
) -> anyhow::Result<task::TaskVariables> {
|
||||
let package_json_path = worktree_root.join("package.json");
|
||||
let metadata = fs
|
||||
.metadata(&package_json_path)
|
||||
.await
|
||||
.with_context(|| format!("getting metadata for {package_json_path:?}"))?
|
||||
.with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
|
||||
let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
|
||||
let existing_data = {
|
||||
let contents = package_json_contents.0.read().await;
|
||||
contents
|
||||
.get(&package_json_path)
|
||||
.filter(|package_json| package_json.mtime == mtime)
|
||||
.map(|package_json| package_json.data)
|
||||
};
|
||||
|
||||
let mut variables = TaskVariables::default();
|
||||
if let Some(existing_data) = existing_data {
|
||||
existing_data.fill_variables(&mut variables);
|
||||
} else {
|
||||
let package_json_string = fs
|
||||
.load(&package_json_path)
|
||||
.await
|
||||
.with_context(|| format!("loading package.json from {package_json_path:?}"))?;
|
||||
let package_json: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_str(&package_json_string)
|
||||
.with_context(|| format!("parsing package.json from {package_json_path:?}"))?;
|
||||
let new_data = PackageJsonData::new(package_json);
|
||||
new_data.fill_variables(&mut variables);
|
||||
{
|
||||
let mut contents = package_json_contents.0.write().await;
|
||||
contents.insert(
|
||||
package_json_path,
|
||||
PackageJson {
|
||||
mtime,
|
||||
data: new_data,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(variables)
|
||||
}
|
||||
|
||||
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
|
||||
@@ -225,7 +225,7 @@ impl Markdown {
|
||||
self.parse(cx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
|
||||
&self.parsed_markdown
|
||||
}
|
||||
|
||||
@@ -5753,15 +5753,28 @@ impl MultiBufferSnapshot {
|
||||
let mut result = Vec::new();
|
||||
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
|
||||
|
||||
let mut prev_settings = None;
|
||||
while let Some((first_row, mut line_indent, buffer)) = row_indents.next() {
|
||||
if first_row > end_row {
|
||||
break;
|
||||
}
|
||||
let current_depth = indent_stack.len() as u32;
|
||||
|
||||
let settings =
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx);
|
||||
let tab_size = settings.tab_size.get() as u32;
|
||||
// Avoid retrieving the language settings repeatedly for every buffer row.
|
||||
if let Some((prev_buffer_id, _)) = &prev_settings {
|
||||
if prev_buffer_id != &buffer.remote_id() {
|
||||
prev_settings.take();
|
||||
}
|
||||
}
|
||||
let settings = &prev_settings
|
||||
.get_or_insert_with(|| {
|
||||
(
|
||||
buffer.remote_id(),
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx),
|
||||
)
|
||||
})
|
||||
.1;
|
||||
let tab_size = settings.tab_size.get();
|
||||
|
||||
// When encountering empty, continue until found useful line indent
|
||||
// then add to the indent stack with the depth found
|
||||
|
||||
@@ -408,6 +408,7 @@ pub fn task_file_name() -> &'static str {
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `debug.json` file within a project.
|
||||
/// .zed/debug.json
|
||||
pub fn local_debug_file_relative_path() -> &'static Path {
|
||||
Path::new(".zed/debug.json")
|
||||
}
|
||||
|
||||
@@ -103,8 +103,9 @@ impl DapStore {
|
||||
ADD_LOCATORS.call_once(|| {
|
||||
let registry = DapRegistry::global(cx);
|
||||
registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
|
||||
registry.add_locator(Arc::new(locators::python::PythonLocator));
|
||||
registry.add_locator(Arc::new(locators::go::GoLocator {}));
|
||||
registry.add_locator(Arc::new(locators::node::NodeLocator));
|
||||
registry.add_locator(Arc::new(locators::python::PythonLocator));
|
||||
});
|
||||
client.add_entity_request_handler(Self::handle_run_debug_locator);
|
||||
client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub(crate) mod cargo;
|
||||
pub(crate) mod go;
|
||||
pub(crate) mod node;
|
||||
pub(crate) mod python;
|
||||
|
||||
@@ -30,6 +30,47 @@ impl DapLocator for GoLocator {
|
||||
let go_action = build_config.args.first()?;
|
||||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let binary_path = if build_config.env.contains_key("OUT_DIR") {
|
||||
"${OUT_DIR}/__debug".to_string()
|
||||
} else {
|
||||
"__debug".to_string()
|
||||
};
|
||||
|
||||
let build_task = TaskTemplate {
|
||||
label: "go test debug".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-c".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
"-o".into(),
|
||||
binary_path,
|
||||
],
|
||||
env: build_config.env.clone(),
|
||||
cwd: build_config.cwd.clone(),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
Some(DebugScenario {
|
||||
label: resolved_label.to_string().into(),
|
||||
adapter: adapter.0,
|
||||
build: Some(BuildTaskDefinition::Template {
|
||||
task_template: build_task,
|
||||
locator_name: Some(self.name()),
|
||||
}),
|
||||
config: serde_json::Value::Null,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
"run" => {
|
||||
let program = build_config
|
||||
.args
|
||||
@@ -91,6 +132,23 @@ impl DapLocator for GoLocator {
|
||||
}
|
||||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let program = if let Some(out_dir) = build_config.env.get("OUT_DIR") {
|
||||
format!("{}/__debug", out_dir)
|
||||
} else {
|
||||
PathBuf::from(&cwd)
|
||||
.join("__debug")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
Ok(DebugRequest::Launch(task::LaunchRequest {
|
||||
program,
|
||||
cwd: Some(PathBuf::from(&cwd)),
|
||||
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
|
||||
env,
|
||||
}))
|
||||
}
|
||||
"build" => {
|
||||
let package = build_config
|
||||
.args
|
||||
@@ -221,6 +279,92 @@ mod tests {
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_test() {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go test".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
assert_eq!(scenario.adapter, "Delve");
|
||||
assert_eq!(scenario.label, "test label");
|
||||
assert!(scenario.build.is_some());
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert_eq!(task_template.command, "go");
|
||||
assert!(task_template.args.contains(&"test".into()));
|
||||
assert!(task_template.args.contains(&"-c".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
||||
);
|
||||
assert!(task_template.args.contains(&"-o".into()));
|
||||
assert!(task_template.args.contains(&"__debug".into()));
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
|
||||
assert!(
|
||||
scenario.config.is_null(),
|
||||
"Initial config should be null to ensure it's invalid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_test_with_out_dir() {
|
||||
let locator = GoLocator;
|
||||
let mut env = FxHashMap::default();
|
||||
env.insert("OUT_DIR".to_string(), "/tmp/build".to_string());
|
||||
|
||||
let task = TaskTemplate {
|
||||
label: "go test".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env,
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert!(task_template.args.contains(&"${OUT_DIR}/__debug".into()));
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_unsupported_go_commands() {
|
||||
let locator = GoLocator;
|
||||
|
||||
68
crates/project/src/debugger/locators/node.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
|
||||
|
||||
pub(crate) struct NodeLocator;
|
||||
|
||||
const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
|
||||
|
||||
#[async_trait]
|
||||
impl DapLocator for NodeLocator {
|
||||
fn name(&self) -> SharedString {
|
||||
SharedString::new_static("Node")
|
||||
}
|
||||
|
||||
/// Determines whether this locator can generate debug target for given task.
|
||||
fn create_scenario(
|
||||
&self,
|
||||
build_config: &TaskTemplate,
|
||||
resolved_label: &str,
|
||||
adapter: DebugAdapterName,
|
||||
) -> Option<DebugScenario> {
|
||||
// TODO(debugger) fix issues with `await` breakpoint step
|
||||
if cfg!(not(debug_assertions)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if adapter.as_ref() != "JavaScript" {
|
||||
return None;
|
||||
}
|
||||
if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() {
|
||||
return None;
|
||||
}
|
||||
let test_library = build_config.args.first()?;
|
||||
let program_path = Path::new("$ZED_WORKTREE_ROOT")
|
||||
.join("node_modules")
|
||||
.join(".bin")
|
||||
.join(test_library);
|
||||
let args = build_config.args[1..].to_vec();
|
||||
|
||||
let config = serde_json::json!({
|
||||
"request": "launch",
|
||||
"type": "pwa-node",
|
||||
"program": program_path,
|
||||
"args": args,
|
||||
"cwd": build_config.cwd.clone(),
|
||||
"runtimeArgs": ["--inspect-brk"],
|
||||
"console": "integratedTerminal",
|
||||
});
|
||||
|
||||
Some(DebugScenario {
|
||||
adapter: adapter.0,
|
||||
label: resolved_label.to_string().into(),
|
||||
build: None,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn run(&self, _: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
bail!("Python locator should not require DapLocator::run to be ran");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
|
||||
|
||||
pub(crate) struct PythonLocator;
|
||||
|
||||
@@ -35,6 +35,13 @@ impl DapLocator for PythonLocator {
|
||||
// We cannot debug selections.
|
||||
return None;
|
||||
}
|
||||
let command = if build_config.command
|
||||
== VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value()
|
||||
{
|
||||
VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value()
|
||||
} else {
|
||||
build_config.command.clone()
|
||||
};
|
||||
let module_specifier_position = build_config
|
||||
.args
|
||||
.iter()
|
||||
@@ -68,7 +75,7 @@ impl DapLocator for PythonLocator {
|
||||
}
|
||||
let mut config = serde_json::json!({
|
||||
"request": "launch",
|
||||
"python": build_config.command,
|
||||
"python": command,
|
||||
"args": args,
|
||||
"cwd": build_config.cwd.clone()
|
||||
});
|
||||
|
||||
@@ -121,16 +121,17 @@ impl From<dap::Thread> for Thread {
|
||||
|
||||
pub enum Mode {
|
||||
Building,
|
||||
Running(LocalMode),
|
||||
Running(RunningMode),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalMode {
|
||||
pub struct RunningMode {
|
||||
client: Arc<DebugAdapterClient>,
|
||||
binary: DebugAdapterBinary,
|
||||
tmp_breakpoint: Option<SourceBreakpoint>,
|
||||
worktree: WeakEntity<Worktree>,
|
||||
executor: BackgroundExecutor,
|
||||
is_started: bool,
|
||||
}
|
||||
|
||||
fn client_source(abs_path: &Path) -> dap::Source {
|
||||
@@ -148,7 +149,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalMode {
|
||||
impl RunningMode {
|
||||
async fn new(
|
||||
session_id: SessionId,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
@@ -181,6 +182,7 @@ impl LocalMode {
|
||||
tmp_breakpoint: None,
|
||||
binary,
|
||||
executor: cx.background_executor().clone(),
|
||||
is_started: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,7 +375,7 @@ impl LocalMode {
|
||||
capabilities: &Capabilities,
|
||||
initialized_rx: oneshot::Receiver<()>,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
cx: &App,
|
||||
cx: &mut Context<Session>,
|
||||
) -> Task<Result<()>> {
|
||||
let raw = self.binary.request_args.clone();
|
||||
|
||||
@@ -405,7 +407,7 @@ impl LocalMode {
|
||||
let this = self.clone();
|
||||
let worktree = self.worktree().clone();
|
||||
let configuration_sequence = cx.spawn({
|
||||
async move |cx| {
|
||||
async move |_, cx| {
|
||||
let breakpoint_store =
|
||||
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
|
||||
initialized_rx.await?;
|
||||
@@ -453,9 +455,20 @@ impl LocalMode {
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
futures::future::try_join(launch, configuration_sequence).await?;
|
||||
Ok(())
|
||||
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(this) = this.as_running_mut() {
|
||||
this.is_started = true;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -704,7 +717,7 @@ impl Session {
|
||||
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
|
||||
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local
|
||||
@@ -714,7 +727,7 @@ impl Session {
|
||||
}
|
||||
BreakpointStoreEvent::BreakpointsCleared(paths) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local.unset_breakpoints_from_paths(paths, cx).detach();
|
||||
@@ -806,7 +819,7 @@ impl Session {
|
||||
let parent_session = self.parent_session.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mode = LocalMode::new(
|
||||
let mode = RunningMode::new(
|
||||
id,
|
||||
parent_session,
|
||||
worktree.downgrade(),
|
||||
@@ -906,18 +919,29 @@ impl Session {
|
||||
return tx;
|
||||
}
|
||||
|
||||
pub fn is_local(&self) -> bool {
|
||||
pub fn is_started(&self) -> bool {
|
||||
match &self.mode {
|
||||
Mode::Building => false,
|
||||
Mode::Running(running) => running.is_started,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_building(&self) -> bool {
|
||||
matches!(self.mode, Mode::Building)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
matches!(self.mode, Mode::Running(_))
|
||||
}
|
||||
|
||||
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
|
||||
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
|
||||
match &mut self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_local(&self) -> Option<&LocalMode> {
|
||||
pub fn as_running(&self) -> Option<&RunningMode> {
|
||||
match &self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
@@ -1140,7 +1164,7 @@ impl Session {
|
||||
body: Option<serde_json::Value>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(local_session) = self.as_local() else {
|
||||
let Some(local_session) = self.as_running() else {
|
||||
unreachable!("Cannot respond to remote client");
|
||||
};
|
||||
let client = local_session.client.clone();
|
||||
@@ -1162,7 +1186,7 @@ impl Session {
|
||||
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
|
||||
// todo(debugger): Find a clean way to get around the clone
|
||||
let breakpoint_store = self.breakpoint_store.clone();
|
||||
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
|
||||
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
|
||||
let breakpoint = local.tmp_breakpoint.take()?;
|
||||
let path = breakpoint.path.clone();
|
||||
Some((local, path))
|
||||
@@ -1397,7 +1421,7 @@ impl Session {
|
||||
));
|
||||
return cx.spawn(async move |this, cx| {
|
||||
this.update(cx, |this, cx| process_result(this, error, cx))
|
||||
.log_err()
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
}
|
||||
@@ -1406,7 +1430,7 @@ impl Session {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = request.await;
|
||||
this.update(cx, |this, cx| process_result(this, result, cx))
|
||||
.log_err()
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
@@ -1528,7 +1552,7 @@ impl Session {
|
||||
|
||||
self.ignore_breakpoints = ignore;
|
||||
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
|
||||
} else {
|
||||
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
|
||||
@@ -1550,7 +1574,7 @@ impl Session {
|
||||
}
|
||||
|
||||
fn send_exception_breakpoints(&mut self, cx: &App) {
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
let exception_filters = self
|
||||
.exception_breakpoints
|
||||
.values()
|
||||
@@ -2170,4 +2194,8 @@ impl Session {
|
||||
self.shutdown(cx).detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
|
||||
self.thread_states.thread_state(thread_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use dap::DapRegistry;
|
||||
use gpui::{App, AppContext as _, Entity, SharedString, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location,
|
||||
Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
|
||||
language_settings::language_settings,
|
||||
};
|
||||
use lsp::{LanguageServerId, LanguageServerName};
|
||||
@@ -132,7 +132,10 @@ pub enum TaskSourceKind {
|
||||
/// Languages-specific tasks coming from extensions.
|
||||
Language { name: SharedString },
|
||||
/// Language-specific tasks coming from LSP servers.
|
||||
Lsp(LanguageServerId),
|
||||
Lsp {
|
||||
language_name: SharedString,
|
||||
server: LanguageServerId,
|
||||
},
|
||||
}
|
||||
|
||||
/// A collection of task contexts, derived from the current state of the workspace.
|
||||
@@ -211,7 +214,10 @@ impl TaskSourceKind {
|
||||
format!("{id_base}_{id}_{}", directory_in_worktree.display())
|
||||
}
|
||||
Self::Language { name } => format!("language_{name}"),
|
||||
Self::Lsp(server_id) => format!("lsp_{server_id}"),
|
||||
Self::Lsp {
|
||||
server,
|
||||
language_name,
|
||||
} => format!("lsp_{language_name}_{server}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -693,7 +699,7 @@ fn task_lru_comparator(
|
||||
|
||||
fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
|
||||
match kind {
|
||||
TaskSourceKind::Lsp(..) => 0,
|
||||
TaskSourceKind::Lsp { .. } => 0,
|
||||
TaskSourceKind::Language { .. } => 1,
|
||||
TaskSourceKind::UserInput => 2,
|
||||
TaskSourceKind::Worktree { .. } => 3,
|
||||
@@ -791,11 +797,12 @@ impl ContextProvider for BasicContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
_: &TaskVariables,
|
||||
location: &Location,
|
||||
location: ContextLocation<'_>,
|
||||
_: Option<HashMap<String, String>>,
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<TaskVariables>> {
|
||||
let location = location.file_location;
|
||||
let buffer = location.buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
|
||||
|
||||