Compare commits
61 Commits
tool-input
...
theme-chao
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d62f5a58c | ||
|
|
bef7ba8e35 | ||
|
|
ad8b823555 | ||
|
|
139af02737 | ||
|
|
6b7c30d7ad | ||
|
|
1220049089 | ||
|
|
01bdef130b | ||
|
|
9a3720edd3 | ||
|
|
11ddecb995 | ||
|
|
684e14e55b | ||
|
|
263080c4c4 | ||
|
|
925464cfc6 | ||
|
|
bcac748c2b | ||
|
|
0ca0914cca | ||
|
|
8bd739d869 | ||
|
|
60e9ab8f93 | ||
|
|
3327f90e0f | ||
|
|
5e15c05a9d | ||
|
|
1f3575ad6e | ||
|
|
f1db3b4e1d | ||
|
|
02d0e725a8 | ||
|
|
211d6205b9 | ||
|
|
4693f16759 | ||
|
|
c0dc758f24 | ||
|
|
9b7632d5f6 | ||
|
|
877ef5e1b1 | ||
|
|
66a1c356bf | ||
|
|
a9107dfaeb | ||
|
|
d549993c73 | ||
|
|
e0c860c42a | ||
|
|
2a6ef006f4 | ||
|
|
38febed02d | ||
|
|
8cc3b094d2 | ||
|
|
d87603dd60 | ||
|
|
de9053c7ca | ||
|
|
ddf3d99265 | ||
|
|
c35af6c2e2 | ||
|
|
6cb382c49f | ||
|
|
966e75b610 | ||
|
|
861ca05fb9 | ||
|
|
f785853239 | ||
|
|
82aee6bcf7 | ||
|
|
955580dae6 | ||
|
|
c99e42a3d6 | ||
|
|
018dbfba09 | ||
|
|
30a441b714 | ||
|
|
83562fca77 | ||
|
|
6b456ede49 | ||
|
|
6efc5ecefe | ||
|
|
2246b01c4b | ||
|
|
01295aa687 | ||
|
|
66e45818af | ||
|
|
76fe33245f | ||
|
|
44d1f512f8 | ||
|
|
69fd23e947 | ||
|
|
4ad47fc9cc | ||
|
|
0555bbd0ec | ||
|
|
d3da0a809e | ||
|
|
31ec7ef2ec | ||
|
|
75928f4859 | ||
|
|
543a7b123a |
@@ -33,7 +33,6 @@ workspace-members = [
|
||||
"zed_emmet",
|
||||
"zed_glsl",
|
||||
"zed_html",
|
||||
"perplexity",
|
||||
"zed_proto",
|
||||
"zed_ruff",
|
||||
"slash_commands_example",
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
else
|
||||
echo "run_docs=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then
|
||||
echo "run_license=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
"build": {
|
||||
"label": "Build Zed",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build"
|
||||
]
|
||||
"args": ["build"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -16,9 +14,7 @@
|
||||
"build": {
|
||||
"label": "Build Zed",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build"
|
||||
]
|
||||
"args": ["build"]
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -538,6 +538,8 @@ dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"net",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
@@ -4324,6 +4326,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
@@ -4344,6 +4347,7 @@ dependencies = [
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-go",
|
||||
@@ -8951,6 +8955,7 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -9023,7 +9028,6 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"lsp",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
@@ -10230,6 +10234,18 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-io",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"windows 0.61.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
@@ -11335,14 +11351,6 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "perplexity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.8.0"
|
||||
@@ -12534,6 +12542,7 @@ dependencies = [
|
||||
"prost 0.9.0",
|
||||
"prost-build 0.9.0",
|
||||
"serde",
|
||||
"typed-path",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -13197,6 +13206,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"git2",
|
||||
"git_hosting_providers",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
@@ -17035,6 +17045,12 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -18281,6 +18297,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -18358,7 +18375,6 @@ dependencies = [
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
@@ -20144,9 +20160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
|
||||
checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -99,6 +99,7 @@ members = [
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
"crates/net",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
@@ -188,7 +189,6 @@ members = [
|
||||
"extensions/emmet",
|
||||
"extensions/glsl",
|
||||
"extensions/html",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
@@ -311,6 +311,7 @@ menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
mistral = { path = "crates/mistral" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
net = { path = "crates/net" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
@@ -625,7 +626,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.5"
|
||||
zed_llm_client = "= 0.8.6"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
@@ -660,6 +661,7 @@ features = [
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.98749 1.67322C7.08029 1.71878 7.15543 1.79374 7.20121 1.88643C7.24699 1.97912 7.26084 2.08434 7.24061 2.18572L6.72812 4.75007H9.28122C9.37107 4.75006 9.45903 4.77588 9.53463 4.82445C9.61022 4.87302 9.67027 4.94229 9.70761 5.02402C9.74495 5.10574 9.75801 5.19648 9.74524 5.28542C9.73247 5.37437 9.69441 5.45776 9.63559 5.52569L5.57313 10.2131C5.50536 10.2912 5.41366 10.3447 5.31233 10.3653C5.211 10.3858 5.10571 10.3723 5.01285 10.3268C4.92 10.2813 4.8448 10.2064 4.79896 10.1137C4.75311 10.021 4.7392 9.9158 4.75939 9.81439L5.27188 7.25004H2.71878C2.62893 7.25005 2.54097 7.22423 2.46537 7.17566C2.38978 7.12709 2.32973 7.05782 2.29239 6.97609C2.25505 6.89437 2.24199 6.80363 2.25476 6.71469C2.26753 6.62574 2.30559 6.54235 2.36441 6.47443L6.42687 1.78697C6.49466 1.70879 6.58641 1.65524 6.68782 1.63467C6.78923 1.61409 6.89459 1.62765 6.98749 1.67322Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 1.0 KiB |
@@ -189,6 +189,8 @@
|
||||
"z shift-r": "editor::UnfoldAll",
|
||||
"z l": "vim::ColumnRight",
|
||||
"z h": "vim::ColumnLeft",
|
||||
"z shift-l": "vim::HalfPageRight",
|
||||
"z shift-h": "vim::HalfPageLeft",
|
||||
"shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
|
||||
"shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
|
||||
// Count support
|
||||
@@ -218,35 +220,18 @@
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Cancel",
|
||||
"escape": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"c": "vim::PushChange",
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": "vim::PushDelete",
|
||||
"delete": "vim::DeleteRight",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"y": "vim::PushYank",
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
"~": "vim::ChangeCase",
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"p": "vim::Paste",
|
||||
"shift-p": ["vim::Paste", { "before": true }],
|
||||
"u": "vim::Undo",
|
||||
"ctrl-r": "vim::Redo",
|
||||
"r": "vim::PushReplace",
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": "vim::PushIndent",
|
||||
"<": "vim::PushOutdent",
|
||||
"=": "vim::PushAutoIndent",
|
||||
@@ -256,11 +241,8 @@
|
||||
"g ~": "vim::PushOppositeCase",
|
||||
"g ?": "vim::PushRot13",
|
||||
// "g ?": "vim::PushRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
"g w": "vim::PushRewrap",
|
||||
"g q": "vim::PushRewrap",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
@@ -327,6 +309,7 @@
|
||||
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g w": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
@@ -363,18 +346,11 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"left": "vim::WrappingLeft",
|
||||
"right": "vim::WrappingRight",
|
||||
"h": "vim::WrappingLeft",
|
||||
"l": "vim::WrappingRight",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": "editor::Copy",
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
@@ -388,27 +364,39 @@
|
||||
"p": "vim::Paste",
|
||||
"shift-p": ["vim::Paste", { "before": true }],
|
||||
"u": "vim::Undo",
|
||||
"r": "vim::PushReplace",
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"\"": "vim::PushRegister",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"left": "vim::WrappingLeft",
|
||||
"right": "vim::WrappingRight",
|
||||
"h": "vim::WrappingLeft",
|
||||
"l": "vim::WrappingRight",
|
||||
"y": "editor::Copy",
|
||||
"alt-;": "vim::OtherEnd",
|
||||
"ctrl-r": "vim::Redo",
|
||||
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
|
||||
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
|
||||
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
|
||||
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
|
||||
"r": "vim::PushReplace",
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"=": "vim::AutoIndent",
|
||||
"g u": "vim::PushLowercase",
|
||||
"g shift-u": "vim::PushUppercase",
|
||||
"g ~": "vim::PushOppositeCase",
|
||||
"\"": "vim::PushRegister",
|
||||
"g q": "vim::PushRewrap",
|
||||
"g w": "vim::PushRewrap",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
@@ -428,7 +416,6 @@
|
||||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
|
||||
@@ -617,6 +617,8 @@
|
||||
// 3. Mark files with errors and warnings:
|
||||
// "all"
|
||||
"show_diagnostics": "all",
|
||||
// Whether to stick parent directories at top of the project panel.
|
||||
"sticky_scroll": true,
|
||||
// Settings related to indent guides in the project panel.
|
||||
"indent_guides": {
|
||||
// When to show indent guides in the project panel.
|
||||
@@ -808,6 +810,7 @@
|
||||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"move_path": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
@@ -827,6 +830,7 @@
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
@@ -853,7 +857,15 @@
|
||||
// its response, or needs user input.
|
||||
|
||||
// Default: false
|
||||
"play_sound_when_agent_done": false
|
||||
"play_sound_when_agent_done": false,
|
||||
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_edit_card": true,
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_terminal_card": true
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -1348,7 +1360,7 @@
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
}
|
||||
},
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": 15,
|
||||
@@ -1365,6 +1377,21 @@
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
// "max_scroll_history_lines": 10000,
|
||||
// The minimum APCA perceptual contrast between foreground and background colors.
|
||||
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
|
||||
// especially for dark mode. Values range from 0 to 106.
|
||||
//
|
||||
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
|
||||
// https://readtech.org/ARC/tests/bronze-simple-mode/
|
||||
// - 0: No contrast adjustment
|
||||
// - 45: Minimum for large fluent text (36px+)
|
||||
// - 60: Minimum for other content text
|
||||
// - 75: Minimum for body text
|
||||
// - 90: Preferred for body text
|
||||
//
|
||||
// Most terminal themes have APCA values of 40-70.
|
||||
// A value of 45 preserves colorful themes while ensuring legibility.
|
||||
"minimum_contrast": 45
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Settings related to running tasks.
|
||||
@@ -1576,6 +1603,9 @@
|
||||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1589,6 +1619,9 @@
|
||||
}
|
||||
},
|
||||
"Plain Text": {
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[
|
||||
{
|
||||
"label": "Debug active PHP file",
|
||||
"adapter": "PHP",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active Python file",
|
||||
"adapter": "Debugpy",
|
||||
|
||||
@@ -6,398 +6,7 @@
|
||||
{
|
||||
"name": "One Dark",
|
||||
"appearance": "dark",
|
||||
"style": {
|
||||
"border": "#464b57ff",
|
||||
"border.variant": "#363c46ff",
|
||||
"border.focused": "#47679eff",
|
||||
"border.selected": "#293b5bff",
|
||||
"border.transparent": "#00000000",
|
||||
"border.disabled": "#414754ff",
|
||||
"elevated_surface.background": "#2f343eff",
|
||||
"surface.background": "#2f343eff",
|
||||
"background": "#3b414dff",
|
||||
"element.background": "#2e343eff",
|
||||
"element.hover": "#363c46ff",
|
||||
"element.active": "#454a56ff",
|
||||
"element.selected": "#454a56ff",
|
||||
"element.disabled": "#2e343eff",
|
||||
"drop_target.background": "#83899480",
|
||||
"ghost_element.background": "#00000000",
|
||||
"ghost_element.hover": "#363c46ff",
|
||||
"ghost_element.active": "#454a56ff",
|
||||
"ghost_element.selected": "#454a56ff",
|
||||
"ghost_element.disabled": "#2e343eff",
|
||||
"text": "#dce0e5ff",
|
||||
"text.muted": "#a9afbcff",
|
||||
"text.placeholder": "#878a98ff",
|
||||
"text.disabled": "#878a98ff",
|
||||
"text.accent": "#74ade8ff",
|
||||
"icon": "#dce0e5ff",
|
||||
"icon.muted": "#a9afbcff",
|
||||
"icon.disabled": "#878a98ff",
|
||||
"icon.placeholder": "#a9afbcff",
|
||||
"icon.accent": "#74ade8ff",
|
||||
"status_bar.background": "#3b414dff",
|
||||
"title_bar.background": "#3b414dff",
|
||||
"title_bar.inactive_background": "#2e343eff",
|
||||
"toolbar.background": "#282c33ff",
|
||||
"tab_bar.background": "#2f343eff",
|
||||
"tab.inactive_background": "#2f343eff",
|
||||
"tab.active_background": "#282c33ff",
|
||||
"search.match_background": "#74ade866",
|
||||
"panel.background": "#2f343eff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar.thumb.background": "#c8ccd44c",
|
||||
"scrollbar.thumb.hover_background": "#363c46ff",
|
||||
"scrollbar.thumb.border": "#363c46ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
"scrollbar.track.border": "#2e333cff",
|
||||
"editor.foreground": "#acb2beff",
|
||||
"editor.background": "#282c33ff",
|
||||
"editor.gutter.background": "#282c33ff",
|
||||
"editor.subheader.background": "#2f343eff",
|
||||
"editor.active_line.background": "#2f343ebf",
|
||||
"editor.highlighted_line.background": "#2f343eff",
|
||||
"editor.line_number": "#4e5a5f",
|
||||
"editor.active_line_number": "#d0d4da",
|
||||
"editor.hover_line_number": "#acb0b4",
|
||||
"editor.invisible": "#878a98ff",
|
||||
"editor.wrap_guide": "#c8ccd40d",
|
||||
"editor.active_wrap_guide": "#c8ccd41a",
|
||||
"editor.document_highlight.read_background": "#74ade81a",
|
||||
"editor.document_highlight.write_background": "#555a6366",
|
||||
"terminal.background": "#282c33ff",
|
||||
"terminal.foreground": "#dce0e5ff",
|
||||
"terminal.bright_foreground": "#dce0e5ff",
|
||||
"terminal.dim_foreground": "#282c33ff",
|
||||
"terminal.ansi.black": "#282c33ff",
|
||||
"terminal.ansi.bright_black": "#525561ff",
|
||||
"terminal.ansi.dim_black": "#dce0e5ff",
|
||||
"terminal.ansi.red": "#d07277ff",
|
||||
"terminal.ansi.bright_red": "#673a3cff",
|
||||
"terminal.ansi.dim_red": "#eab7b9ff",
|
||||
"terminal.ansi.green": "#a1c181ff",
|
||||
"terminal.ansi.bright_green": "#4d6140ff",
|
||||
"terminal.ansi.dim_green": "#d1e0bfff",
|
||||
"terminal.ansi.yellow": "#dec184ff",
|
||||
"terminal.ansi.bright_yellow": "#e5c07bff",
|
||||
"terminal.ansi.dim_yellow": "#f1dfc1ff",
|
||||
"terminal.ansi.blue": "#74ade8ff",
|
||||
"terminal.ansi.bright_blue": "#385378ff",
|
||||
"terminal.ansi.dim_blue": "#bed5f4ff",
|
||||
"terminal.ansi.magenta": "#be5046ff",
|
||||
"terminal.ansi.bright_magenta": "#5e2b26ff",
|
||||
"terminal.ansi.dim_magenta": "#e6a79eff",
|
||||
"terminal.ansi.cyan": "#6eb4bfff",
|
||||
"terminal.ansi.bright_cyan": "#3a565bff",
|
||||
"terminal.ansi.dim_cyan": "#b9d9dfff",
|
||||
"terminal.ansi.white": "#dce0e5ff",
|
||||
"terminal.ansi.bright_white": "#dce0e5ff",
|
||||
"terminal.ansi.dim_white": "#575d65ff",
|
||||
"link_text.hover": "#74ade8ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
"created": "#a1c181ff",
|
||||
"created.background": "#a1c1811a",
|
||||
"created.border": "#38482fff",
|
||||
"deleted": "#d07277ff",
|
||||
"deleted.background": "#d072771a",
|
||||
"deleted.border": "#4c2b2cff",
|
||||
"error": "#d07277ff",
|
||||
"error.background": "#d072771a",
|
||||
"error.border": "#4c2b2cff",
|
||||
"hidden": "#878a98ff",
|
||||
"hidden.background": "#696b771a",
|
||||
"hidden.border": "#414754ff",
|
||||
"hint": "#788ca6ff",
|
||||
"hint.background": "#5a6f891a",
|
||||
"hint.border": "#293b5bff",
|
||||
"ignored": "#878a98ff",
|
||||
"ignored.background": "#696b771a",
|
||||
"ignored.border": "#464b57ff",
|
||||
"info": "#74ade8ff",
|
||||
"info.background": "#74ade81a",
|
||||
"info.border": "#293b5bff",
|
||||
"modified": "#dec184ff",
|
||||
"modified.background": "#dec1841a",
|
||||
"modified.border": "#5d4c2fff",
|
||||
"predictive": "#5a6a87ff",
|
||||
"predictive.background": "#5a6a871a",
|
||||
"predictive.border": "#38482fff",
|
||||
"renamed": "#74ade8ff",
|
||||
"renamed.background": "#74ade81a",
|
||||
"renamed.border": "#293b5bff",
|
||||
"success": "#a1c181ff",
|
||||
"success.background": "#a1c1811a",
|
||||
"success.border": "#38482fff",
|
||||
"unreachable": "#a9afbcff",
|
||||
"unreachable.background": "#8389941a",
|
||||
"unreachable.border": "#464b57ff",
|
||||
"warning": "#dec184ff",
|
||||
"warning.background": "#dec1841a",
|
||||
"warning.border": "#5d4c2fff",
|
||||
"players": [
|
||||
{
|
||||
"cursor": "#74ade8ff",
|
||||
"background": "#74ade8ff",
|
||||
"selection": "#74ade83d"
|
||||
},
|
||||
{
|
||||
"cursor": "#be5046ff",
|
||||
"background": "#be5046ff",
|
||||
"selection": "#be50463d"
|
||||
},
|
||||
{
|
||||
"cursor": "#bf956aff",
|
||||
"background": "#bf956aff",
|
||||
"selection": "#bf956a3d"
|
||||
},
|
||||
{
|
||||
"cursor": "#b477cfff",
|
||||
"background": "#b477cfff",
|
||||
"selection": "#b477cf3d"
|
||||
},
|
||||
{
|
||||
"cursor": "#6eb4bfff",
|
||||
"background": "#6eb4bfff",
|
||||
"selection": "#6eb4bf3d"
|
||||
},
|
||||
{
|
||||
"cursor": "#d07277ff",
|
||||
"background": "#d07277ff",
|
||||
"selection": "#d072773d"
|
||||
},
|
||||
{
|
||||
"cursor": "#dec184ff",
|
||||
"background": "#dec184ff",
|
||||
"selection": "#dec1843d"
|
||||
},
|
||||
{
|
||||
"cursor": "#a1c181ff",
|
||||
"background": "#a1c181ff",
|
||||
"selection": "#a1c1813d"
|
||||
}
|
||||
],
|
||||
"syntax": {
|
||||
"attribute": {
|
||||
"color": "#74ade8ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"boolean": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#5d636fff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"comment.doc": {
|
||||
"color": "#878e98ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"constant": {
|
||||
"color": "#dfc184ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"constructor": {
|
||||
"color": "#73ade9ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"embedded": {
|
||||
"color": "#dce0e5ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#74ade8ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"emphasis.strong": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
},
|
||||
"enum": {
|
||||
"color": "#d07277ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"function": {
|
||||
"color": "#73ade9ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"hint": {
|
||||
"color": "#788ca6ff",
|
||||
"font_style": null,
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#b477cfff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"label": {
|
||||
"color": "#74ade8ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"link_text": {
|
||||
"color": "#73ade9ff",
|
||||
"font_style": "normal",
|
||||
"font_weight": null
|
||||
},
|
||||
"link_uri": {
|
||||
"color": "#6eb4bfff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"namespace": {
|
||||
"color": "#dce0e5ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"number": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"operator": {
|
||||
"color": "#6eb4bfff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"predictive": {
|
||||
"color": "#5a6a87ff",
|
||||
"font_style": "italic",
|
||||
"font_weight": null
|
||||
},
|
||||
"preproc": {
|
||||
"color": "#dce0e5ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"primary": {
|
||||
"color": "#acb2beff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"property": {
|
||||
"color": "#d07277ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation": {
|
||||
"color": "#acb2beff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.bracket": {
|
||||
"color": "#b2b9c6ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.delimiter": {
|
||||
"color": "#b2b9c6ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.list_marker": {
|
||||
"color": "#d07277ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#b1574bff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"selector": {
|
||||
"color": "#dfc184ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"selector.pseudo": {
|
||||
"color": "#74ade8ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"string": {
|
||||
"color": "#a1c181ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"string.escape": {
|
||||
"color": "#878e98ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"string.regex": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"string.special": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"string.special.symbol": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"tag": {
|
||||
"color": "#74ade8ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"text.literal": {
|
||||
"color": "#a1c181ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"title": {
|
||||
"color": "#d07277ff",
|
||||
"font_style": null,
|
||||
"font_weight": 400
|
||||
},
|
||||
"type": {
|
||||
"color": "#6eb4bfff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable": {
|
||||
"color": "#acb2beff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#bf956aff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#73ade9ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
}
|
||||
}
|
||||
}
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"name": "One Light",
|
||||
|
||||
3
crates/agent/src/prompts/stale_files_prompt_header.txt
Normal file
3
crates/agent/src/prompts/stale_files_prompt_header.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
@@ -23,10 +23,11 @@ use gpui::{
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
|
||||
LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
|
||||
Role, SelectedModel, StopReason, TokenUsage,
|
||||
LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
|
||||
TokenUsage,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::{
|
||||
@@ -45,7 +46,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use util::{ResultExt as _, debug_panic, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
@@ -1248,6 +1249,8 @@ impl Thread {
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
self.flush_notifications(model.clone(), intent, cx);
|
||||
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
|
||||
self.stream_completion(request, model, intent, window, cx);
|
||||
@@ -1481,6 +1484,111 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
/// Insert auto-generated notifications (if any) to the thread
|
||||
fn flush_notifications(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match intent {
|
||||
CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
|
||||
if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id: pending_tool_use.id.clone(),
|
||||
pending_tool_use: Some(pending_tool_use),
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionIntent::ThreadSummarization
|
||||
| CompletionIntent::ThreadContextSummarization
|
||||
| CompletionIntent::CreateFile
|
||||
| CompletionIntent::EditFile
|
||||
| CompletionIntent::InlineAssist
|
||||
| CompletionIntent::TerminalInlineAssist
|
||||
| CompletionIntent::GenerateGitCommitMessage => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn attach_tracked_files_state(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let action_log = self.action_log.read(cx);
|
||||
|
||||
action_log.unnotified_stale_buffers(cx).next()?;
|
||||
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
|
||||
debug_panic!("`project_notifications` tool not found");
|
||||
return None;
|
||||
};
|
||||
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input = serde_json::json!({});
|
||||
let request = Arc::new(LanguageModelRequest::default()); // unused
|
||||
let window = None;
|
||||
let tool_result = tool.run(
|
||||
input,
|
||||
request,
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
model.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let tool_use_id =
|
||||
LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
|
||||
|
||||
let tool_use = LanguageModelToolUse {
|
||||
id: tool_use_id.clone(),
|
||||
name: tool_name.clone(),
|
||||
raw_input: "{}".to_string(),
|
||||
input: serde_json::json!({}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
|
||||
let tool_output = cx.background_executor().block(tool_result.output);
|
||||
|
||||
// Attach a project_notification tool call to the latest existing
|
||||
// Assistant message. We cannot create a new Assistant message
|
||||
// because thinking models require a `thinking` block that we
|
||||
// cannot mock. We cannot send a notification as a normal
|
||||
// (non-tool-use) User message because this distracts Agent
|
||||
// too much.
|
||||
let tool_message_id = self
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rfind(|(_, message)| message.role == Role::Assistant)
|
||||
.map(|(_, message)| message.id)?;
|
||||
|
||||
let tool_use_metadata = ToolUseMetadata {
|
||||
model: model.clone(),
|
||||
thread_id: self.id.clone(),
|
||||
prompt_id: self.last_prompt_id.clone(),
|
||||
};
|
||||
|
||||
self.tool_use
|
||||
.request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
|
||||
|
||||
let pending_tool_use = self.tool_use.insert_tool_output(
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
tool_output,
|
||||
self.configured_model.as_ref(),
|
||||
self.completion_mode,
|
||||
);
|
||||
|
||||
pending_tool_use
|
||||
}
|
||||
|
||||
pub fn stream_completion(
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
@@ -1504,6 +1612,10 @@ impl Thread {
|
||||
prompt_id: prompt_id.clone(),
|
||||
};
|
||||
|
||||
let completion_mode = request
|
||||
.mode
|
||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
||||
|
||||
self.last_received_chunk_at = Some(Instant::now());
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
@@ -1853,7 +1965,11 @@ impl Thread {
|
||||
.unwrap_or(0)
|
||||
// We know the context window was exceeded in practice, so if our estimate was
|
||||
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
|
||||
.max(model.max_token_count().saturating_add(1))
|
||||
.max(
|
||||
model
|
||||
.max_token_count_for_mode(completion_mode)
|
||||
.saturating_add(1),
|
||||
)
|
||||
});
|
||||
thread.exceeded_window_error = Some(ExceededWindowError {
|
||||
model_id: model.id(),
|
||||
@@ -2401,6 +2517,7 @@ impl Thread {
|
||||
hallucinated_tool_name,
|
||||
Err(anyhow!("Missing tool call: {error_message}")),
|
||||
self.configured_model.as_ref(),
|
||||
self.completion_mode,
|
||||
);
|
||||
|
||||
cx.emit(ThreadEvent::MissingToolUse {
|
||||
@@ -2427,6 +2544,7 @@ impl Thread {
|
||||
tool_name,
|
||||
Err(anyhow!("Error parsing input JSON: {error}")),
|
||||
self.configured_model.as_ref(),
|
||||
self.completion_mode,
|
||||
);
|
||||
let ui_text = if let Some(pending_tool_use) = &pending_tool_use {
|
||||
pending_tool_use.ui_text.clone()
|
||||
@@ -2502,6 +2620,7 @@ impl Thread {
|
||||
tool_name,
|
||||
output,
|
||||
thread.configured_model.as_ref(),
|
||||
thread.completion_mode,
|
||||
);
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
|
||||
})
|
||||
@@ -2978,7 +3097,9 @@ impl Thread {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
let max = model
|
||||
.model
|
||||
.max_token_count_for_mode(self.completion_mode().into());
|
||||
|
||||
let index = self
|
||||
.messages
|
||||
@@ -3005,7 +3126,9 @@ impl Thread {
|
||||
pub fn total_token_usage(&self) -> Option<TotalTokenUsage> {
|
||||
let model = self.configured_model.as_ref()?;
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
let max = model
|
||||
.model
|
||||
.max_token_count_for_mode(self.completion_mode().into());
|
||||
|
||||
if let Some(exceeded_error) = &self.exceeded_window_error {
|
||||
if model.model.id() == exceeded_error.model_id {
|
||||
@@ -3071,6 +3194,7 @@ impl Thread {
|
||||
tool_name,
|
||||
err,
|
||||
self.configured_model.as_ref(),
|
||||
self.completion_mode,
|
||||
);
|
||||
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
|
||||
}
|
||||
@@ -3156,10 +3280,13 @@ mod tests {
|
||||
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use assistant_tools;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::BoxStream;
|
||||
use gpui::TestAppContext;
|
||||
use http_client;
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
|
||||
@@ -3487,6 +3614,134 @@ fn main() {{
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Add a buffer to the context. This will be a tracked buffer
|
||||
let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context = context_store
|
||||
.read_with(cx, |store, _| store.context().next().cloned())
|
||||
.unwrap();
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
|
||||
// Insert user message and assistant response
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx);
|
||||
thread.insert_assistant_message(
|
||||
vec![MessageSegment::Text("This code prints 42.".into())],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// We shouldn't have a stale buffer notification yet
|
||||
let notifications = thread.read_with(cx, |thread, _| {
|
||||
find_tool_uses(thread, "project_notifications")
|
||||
});
|
||||
assert!(
|
||||
notifications.is_empty(),
|
||||
"Should not have stale buffer notification before buffer is modified"
|
||||
);
|
||||
|
||||
// Modify the buffer
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(1..1, "\n println!(\"Added a new line\");\n")],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Insert another user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"What does the code do now?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Check for the stale buffer warning
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
let notifications = thread.read_with(cx, |thread, _cx| {
|
||||
find_tool_uses(thread, "project_notifications")
|
||||
});
|
||||
|
||||
let [notification] = notifications.as_slice() else {
|
||||
panic!("Should have a `project_notifications` tool use");
|
||||
};
|
||||
|
||||
let Some(notification_content) = notification.content.to_str() else {
|
||||
panic!("`project_notifications` should return text");
|
||||
};
|
||||
|
||||
let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
- code.rs
|
||||
"};
|
||||
assert_eq!(notification_content, expected_content);
|
||||
|
||||
// Insert another user message and flush notifications again
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"Can you tell me more?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// There should be no new notifications (we already flushed one)
|
||||
let notifications = thread.read_with(cx, |thread, _cx| {
|
||||
find_tool_uses(thread, "project_notifications")
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
notifications.len(),
|
||||
1,
|
||||
"Should still have only one notification after second flush - no duplicates"
|
||||
);
|
||||
}
|
||||
|
||||
fn find_tool_uses(thread: &Thread, tool_name: &str) -> Vec<LanguageModelToolResult> {
|
||||
thread
|
||||
.messages()
|
||||
.flat_map(|message| {
|
||||
thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.filter(|result| result.tool_name == tool_name.into())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
@@ -5052,6 +5307,14 @@ fn main() {{
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
ToolRegistry::default_global(cx);
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let http_client = Arc::new(http_client::HttpClientWithUrl::new(
|
||||
http_client::FakeHttpClient::with_200_response(),
|
||||
"http://localhost".to_string(),
|
||||
None,
|
||||
));
|
||||
assistant_tools::init(http_client, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
thread::{MessageId, PromptId, ThreadId},
|
||||
thread_store::SerializedMessage,
|
||||
};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{
|
||||
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
|
||||
@@ -11,8 +12,9 @@ use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{App, Entity, SharedString, Task, Window};
|
||||
use icons::IconName;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
|
||||
ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest,
|
||||
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, Role,
|
||||
};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
@@ -400,6 +402,7 @@ impl ToolUseState {
|
||||
tool_name: Arc<str>,
|
||||
output: Result<ToolResultOutput>,
|
||||
configured_model: Option<&ConfiguredModel>,
|
||||
completion_mode: CompletionMode,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
@@ -426,7 +429,10 @@ impl ToolUseState {
|
||||
|
||||
// Protect from overly large output
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.map(|model| {
|
||||
model.model.max_token_count_for_mode(completion_mode.into()) as usize
|
||||
* BYTES_PER_TOKEN_ESTIMATE
|
||||
})
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let content = match tool_result {
|
||||
|
||||
@@ -67,6 +67,8 @@ pub struct AgentSettings {
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
pub preferred_completion_mode: CompletionMode,
|
||||
pub enable_feedback: bool,
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
@@ -291,6 +293,14 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
enable_feedback: Option<bool>,
|
||||
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
///
|
||||
/// Default: true
|
||||
expand_edit_card: Option<bool>,
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
expand_terminal_card: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -441,6 +451,11 @@ impl Settings for AgentSettings {
|
||||
value.preferred_completion_mode,
|
||||
);
|
||||
merge(&mut settings.enable_feedback, value.enable_feedback);
|
||||
merge(&mut settings.expand_edit_card, value.expand_edit_card);
|
||||
merge(
|
||||
&mut settings.expand_terminal_card,
|
||||
value.expand_terminal_card,
|
||||
);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
|
||||
@@ -436,7 +436,7 @@ impl AgentConfiguration {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
|
||||
let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
|
||||
@@ -426,6 +426,7 @@ impl ContextPicker {
|
||||
this.add_recent_file(project_path.clone(), window, cx);
|
||||
})
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
RecentEntry::Thread(thread) => {
|
||||
@@ -443,6 +444,7 @@ impl ContextPicker {
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider {
|
||||
let mut label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
|
||||
|
||||
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
|
||||
let new_text_len = new_text.len();
|
||||
|
||||
@@ -1160,7 +1160,7 @@ impl MessageEditor {
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("file-name")
|
||||
.id(("file-name", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
@@ -1171,9 +1171,16 @@ impl MessageEditor {
|
||||
.gap_0p5()
|
||||
.children(file_name)
|
||||
.children(file_path),
|
||||
), // TODO: Implement line diff
|
||||
// .child(Label::new("+").color(Color::Created))
|
||||
// .child(Label::new("-").color(Color::Deleted)),
|
||||
)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.handle_file_click(buffer.clone(), window, cx);
|
||||
})
|
||||
}), // TODO: Implement line diff
|
||||
// .child(Label::new("+").color(Color::Created))
|
||||
// .child(Label::new("-").color(Color::Deleted)),
|
||||
//
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -38,8 +38,8 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView,
|
||||
LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -3063,7 +3063,7 @@ fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenStat
|
||||
.default_model()?
|
||||
.model;
|
||||
let token_count = context.read(cx).token_count()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
let max_token_count = model.max_token_count_for_mode(context.read(cx).completion_mode().into());
|
||||
let token_state = if max_token_count.saturating_sub(token_count) == 0 {
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
|
||||
@@ -15,6 +15,8 @@ path = "src/askpass.rs"
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
net.workspace = true
|
||||
parking_lot.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::{ffi::OsStr, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
#[cfg(unix)]
|
||||
use futures::{AsyncBufReadExt as _, io::BufReader};
|
||||
#[cfg(unix)]
|
||||
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use futures::{
|
||||
AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
use smol::fs;
|
||||
#[cfg(unix)]
|
||||
use smol::net::unix::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
|
||||
use util::ResultExt as _;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
@@ -42,41 +35,56 @@ impl AskPassDelegate {
|
||||
Self { tx, _task: task }
|
||||
}
|
||||
|
||||
pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
|
||||
pub async fn ask_password(&mut self, prompt: String) -> Result<String> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx.send((prompt, tx)).await?;
|
||||
Ok(rx.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub struct AskPassSession {
|
||||
script_path: PathBuf,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
script_path: std::path::PathBuf,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper: String,
|
||||
#[cfg(target_os = "windows")]
|
||||
secret: std::sync::Arc<parking_lot::Mutex<String>>,
|
||||
_askpass_task: Task<()>,
|
||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
|
||||
#[cfg(target_os = "windows")]
|
||||
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
|
||||
|
||||
impl AskPassSession {
|
||||
/// This will create a new AskPassSession.
|
||||
/// You must retain this session until the master process exits.
|
||||
#[must_use]
|
||||
pub async fn new(
|
||||
executor: &BackgroundExecutor,
|
||||
mut delegate: AskPassDelegate,
|
||||
) -> anyhow::Result<Self> {
|
||||
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
|
||||
use net::async_net::UnixListener;
|
||||
use util::fs::make_file_executable;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new()));
|
||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
let zed_path = get_shell_safe_zed_path()?;
|
||||
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let zed_path = util::get_shell_safe_zed_path()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("finding current executable path for use in askpass")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_secret = secret.clone();
|
||||
let askpass_task = executor.spawn(async move {
|
||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||
|
||||
@@ -93,10 +101,14 @@ impl AskPassSession {
|
||||
if let Some(password) = delegate
|
||||
.ask_password(prompt.to_string())
|
||||
.await
|
||||
.context("failed to get askpass password")
|
||||
.context("getting askpass password")
|
||||
.log_err()
|
||||
{
|
||||
stream.write_all(password.as_bytes()).await.log_err();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
*askpass_secret.lock() = password;
|
||||
}
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.take() {
|
||||
kill_tx.send(()).log_err();
|
||||
@@ -112,34 +124,49 @@ impl AskPassSession {
|
||||
});
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
);
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
|
||||
fs::write(&askpass_script_path, askpass_script)
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
make_file_executable(&askpass_script_path).await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_helper = format!(
|
||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||
askpass_script_path.display()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
script_path: askpass_script_path,
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
secret,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper,
|
||||
|
||||
_askpass_task: askpass_task,
|
||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||
askpass_opened_rx: Some(askpass_opened_rx),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
&self.script_path
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
&self.askpass_helper
|
||||
}
|
||||
|
||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
let connection_timeout = Duration::from_secs(10);
|
||||
// This is the default timeout setting used by VSCode.
|
||||
let connection_timeout = Duration::from_secs(17);
|
||||
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
||||
let askpass_kill_master_rx = self
|
||||
.askpass_kill_master_rx
|
||||
@@ -158,14 +185,19 @@ impl AskPassSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will return the password that was last set by the askpass script.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_password(&self) -> String {
|
||||
self.secret.lock().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
#[cfg(unix)]
|
||||
pub fn main(socket: &str) {
|
||||
use net::UnixStream;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::exit;
|
||||
|
||||
let mut stream = match UnixStream::connect(socket) {
|
||||
@@ -182,6 +214,10 @@ pub fn main(socket: &str) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
|
||||
buffer.pop();
|
||||
}
|
||||
if buffer.last() != Some(&b'\0') {
|
||||
buffer.push(b'\0');
|
||||
}
|
||||
@@ -202,28 +238,28 @@ pub fn main(socket: &str) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
pub fn main(_socket: &str) {}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub struct AskPassSession {
|
||||
path: PathBuf,
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
impl AskPassSession {
|
||||
pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
path: PathBuf::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
|
||||
AskPassResult::Timedout
|
||||
}
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
zed_exe = zed_path.display(),
|
||||
askpass_socket = askpass_socket.display(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use buffer_diff::BufferDiff;
|
||||
use clock;
|
||||
use collections::BTreeMap;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
||||
@@ -17,6 +18,8 @@ pub struct ActionLog {
|
||||
edited_since_project_diagnostics_check: bool,
|
||||
/// The project this action log is associated with
|
||||
project: Entity<Project>,
|
||||
/// Tracks which buffer versions have already been notified as changed externally
|
||||
notified_versions: BTreeMap<Entity<Buffer>, clock::Global>,
|
||||
}
|
||||
|
||||
impl ActionLog {
|
||||
@@ -26,6 +29,7 @@ impl ActionLog {
|
||||
tracked_buffers: BTreeMap::default(),
|
||||
edited_since_project_diagnostics_check: false,
|
||||
project,
|
||||
notified_versions: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +55,7 @@ impl ActionLog {
|
||||
) -> &mut TrackedBuffer {
|
||||
let status = if is_created {
|
||||
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
|
||||
self.notified_versions.remove(&buffer);
|
||||
match tracked.status {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
@@ -106,7 +111,7 @@ impl ActionLog {
|
||||
TrackedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
diff_base,
|
||||
unreviewed_edits: unreviewed_edits,
|
||||
unreviewed_edits,
|
||||
snapshot: text_snapshot.clone(),
|
||||
status,
|
||||
version: buffer.read(cx).version(),
|
||||
@@ -165,6 +170,7 @@ impl ActionLog {
|
||||
// If the buffer had been edited by a tool, but it got
|
||||
// deleted externally, we want to stop tracking it.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -178,6 +184,7 @@ impl ActionLog {
|
||||
// resurrected externally, we want to clear the edits we
|
||||
// were tracking and reset the buffer's state.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
self.track_buffer_internal(buffer, false, cx);
|
||||
}
|
||||
cx.notify();
|
||||
@@ -483,6 +490,7 @@ impl ActionLog {
|
||||
match tracked_buffer.status {
|
||||
TrackedBufferStatus::Created { .. } => {
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
cx.notify();
|
||||
}
|
||||
TrackedBufferStatus::Modified => {
|
||||
@@ -508,6 +516,7 @@ impl ActionLog {
|
||||
match tracked_buffer.status {
|
||||
TrackedBufferStatus::Deleted => {
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {
|
||||
@@ -616,6 +625,7 @@ impl ActionLog {
|
||||
};
|
||||
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
cx.notify();
|
||||
task
|
||||
}
|
||||
@@ -629,6 +639,7 @@ impl ActionLog {
|
||||
|
||||
// Clear all tracked edits for this buffer and start over as if we just read it.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.notified_versions.remove(&buffer);
|
||||
self.buffer_read(buffer.clone(), cx);
|
||||
cx.notify();
|
||||
save
|
||||
@@ -713,6 +724,33 @@ impl ActionLog {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns stale buffers that haven't been notified yet
|
||||
pub fn unnotified_stale_buffers<'a>(
|
||||
&'a self,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = &'a Entity<Buffer>> {
|
||||
self.stale_buffers(cx).filter(|buffer| {
|
||||
let buffer_entity = buffer.read(cx);
|
||||
self.notified_versions
|
||||
.get(buffer)
|
||||
.map_or(true, |notified_version| {
|
||||
*notified_version != buffer_entity.version
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Marks the given buffers as notified at their current versions
|
||||
pub fn mark_buffers_as_notified(
|
||||
&mut self,
|
||||
buffers: impl IntoIterator<Item = Entity<Buffer>>,
|
||||
cx: &App,
|
||||
) {
|
||||
for buffer in buffers {
|
||||
let version = buffer.read(cx).version.clone();
|
||||
self.notified_versions.insert(buffer, version);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over buffers changed since last read or edited by the model
|
||||
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
|
||||
self.tracked_buffers
|
||||
|
||||
@@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> {
|
||||
// `additionalProperties` defaults to `false` unless explicitly specified.
|
||||
// This prevents models from hallucinating tool parameters.
|
||||
if let Value::Object(obj) = json {
|
||||
if let Some(Value::String(type_str)) = obj.get("type") {
|
||||
if type_str == "object" && !obj.contains_key("additionalProperties") {
|
||||
if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
|
||||
if !obj.contains_key("additionalProperties") {
|
||||
obj.insert("additionalProperties".to_string(), Value::Bool(false));
|
||||
}
|
||||
|
||||
// OpenAI API requires non-missing `properties`
|
||||
if !obj.contains_key("properties") {
|
||||
obj.insert("properties".to_string(), Value::Object(Default::default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -11,6 +11,7 @@ mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod project_notifications_tool;
|
||||
mod read_file_tool;
|
||||
mod schema;
|
||||
mod templates;
|
||||
@@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput};
|
||||
pub use find_path_tool::FindPathToolInput;
|
||||
pub use grep_tool::{GrepTool, GrepToolInput};
|
||||
pub use open_tool::OpenTool;
|
||||
pub use project_notifications_tool::ProjectNotificationsTool;
|
||||
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
|
||||
pub use terminal_tool::TerminalTool;
|
||||
|
||||
@@ -61,6 +63,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(ProjectNotificationsTool);
|
||||
registry.register_tool(FindPathTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(GrepTool);
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
use agent_settings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{
|
||||
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
|
||||
@@ -14,7 +15,7 @@ use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between, px,
|
||||
TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use language::{
|
||||
@@ -515,7 +516,9 @@ pub struct EditFileToolCard {
|
||||
|
||||
impl EditFileToolCard {
|
||||
pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
|
||||
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
@@ -556,7 +559,7 @@ impl EditFileToolCard {
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
error_expanded: None,
|
||||
full_height_expanded: true,
|
||||
full_height_expanded: expand_edit_card,
|
||||
total_lines: None,
|
||||
}
|
||||
}
|
||||
@@ -755,6 +758,13 @@ impl ToolCard for EditFileToolCard {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let running_or_pending = match status {
|
||||
ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
|
||||
|
||||
let path_label_button = h_flex()
|
||||
.id(("edit-tool-path-label-button", self.editor.entity_id()))
|
||||
.w_full()
|
||||
@@ -863,6 +873,18 @@ impl ToolCard for EditFileToolCard {
|
||||
header.bg(codeblock_header_bg)
|
||||
})
|
||||
.child(path_label_button)
|
||||
.when(should_show_loading, |header| {
|
||||
header.pr_1p5().child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(error_message, |header, error_message| {
|
||||
header.child(
|
||||
h_flex()
|
||||
|
||||
224
crates/assistant_tools/src/project_notifications_tool.rs
Normal file
224
crates/assistant_tools/src/project_notifications_tool.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProjectUpdatesToolInput {}
|
||||
|
||||
pub struct ProjectNotificationsTool;
|
||||
|
||||
impl Tool for ProjectNotificationsTool {
|
||||
fn name(&self) -> String {
|
||||
"project_notifications".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
include_str!("./project_notifications_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Envelope
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<ProjectUpdatesToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Check project notifications".into()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let mut stale_files = String::new();
|
||||
let mut notified_buffers = Vec::new();
|
||||
|
||||
for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) {
|
||||
if let Some(file) = stale_file.read(cx).file() {
|
||||
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
|
||||
notified_buffers.push(stale_file.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !notified_buffers.is_empty() {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.mark_buffers_as_notified(notified_buffers, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let response = if stale_files.is_empty() {
|
||||
"No new notifications".to_string()
|
||||
} else {
|
||||
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
|
||||
const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
|
||||
format!("{HEADER}{stale_files}").replace("\r\n", "\n")
|
||||
};
|
||||
|
||||
Task::ready(Ok(response.into())).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::ToolResultContent;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/test"),
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let buffer_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("test/code.rs", cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(buffer_path.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Start tracking the buffer
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
});
|
||||
|
||||
// Run the tool before any changes
|
||||
let tool = Arc::new(ProjectNotificationsTool);
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
|
||||
let request = Arc::new(LanguageModelRequest::default());
|
||||
let tool_input = json!({});
|
||||
|
||||
let result = cx.update(|cx| {
|
||||
tool.clone().run(
|
||||
tool_input.clone(),
|
||||
request.clone(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let response = result.output.await.unwrap();
|
||||
let response_text = match &response.content {
|
||||
ToolResultContent::Text(text) => text.clone(),
|
||||
_ => panic!("Expected text response"),
|
||||
};
|
||||
assert_eq!(
|
||||
response_text.as_str(),
|
||||
"No new notifications",
|
||||
"Tool should return 'No new notifications' when no stale buffers"
|
||||
);
|
||||
|
||||
// Modify the buffer (makes it stale)
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(1..1, "\nChange!\n")], None, cx);
|
||||
});
|
||||
|
||||
// Run the tool again
|
||||
let result = cx.update(|cx| {
|
||||
tool.clone().run(
|
||||
tool_input.clone(),
|
||||
request.clone(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// This time the buffer is stale, so the tool should return a notification
|
||||
let response = result.output.await.unwrap();
|
||||
let response_text = match &response.content {
|
||||
ToolResultContent::Text(text) => text.clone(),
|
||||
_ => panic!("Expected text response"),
|
||||
};
|
||||
|
||||
let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
|
||||
assert_eq!(
|
||||
response_text.as_str(),
|
||||
expected_content,
|
||||
"Tool should return the stale buffer notification"
|
||||
);
|
||||
|
||||
// Run the tool once more without any changes - should get no new notifications
|
||||
let result = cx.update(|cx| {
|
||||
tool.run(
|
||||
tool_input.clone(),
|
||||
request.clone(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let response = result.output.await.unwrap();
|
||||
let response_text = match &response.content {
|
||||
ToolResultContent::Text(text) => text.clone(),
|
||||
_ => panic!("Expected text response"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
response_text.as_str(),
|
||||
"No new notifications",
|
||||
"Tool should return 'No new notifications' when running again without changes"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
assistant_tool::init(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
This tool reports which files have been modified by the user since the agent last accessed them.
|
||||
|
||||
It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates.
|
||||
@@ -0,0 +1,3 @@
|
||||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
@@ -2,12 +2,13 @@ use crate::{
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
use agent_settings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
|
||||
WeakEntity, Window,
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
|
||||
TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
|
||||
};
|
||||
use language::LineEnding;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
@@ -218,7 +219,7 @@ impl Tool for TerminalTool {
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
command: program,
|
||||
command: Some(program),
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
@@ -247,6 +248,7 @@ impl Tool for TerminalTool {
|
||||
command_markdown.clone(),
|
||||
working_dir.clone(),
|
||||
cx.entity_id(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -441,7 +443,10 @@ impl TerminalToolCard {
|
||||
input_command: Entity<Markdown>,
|
||||
working_dir: Option<PathBuf>,
|
||||
entity_id: EntityId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let expand_terminal_card =
|
||||
agent_settings::AgentSettings::get_global(cx).expand_terminal_card;
|
||||
Self {
|
||||
input_command,
|
||||
working_dir,
|
||||
@@ -453,7 +458,7 @@ impl TerminalToolCard {
|
||||
finished_with_empty_output: false,
|
||||
original_content_len: 0,
|
||||
content_line_count: 0,
|
||||
preview_expanded: true,
|
||||
preview_expanded: expand_terminal_card,
|
||||
start_instant: Instant::now(),
|
||||
elapsed_time: None,
|
||||
}
|
||||
@@ -518,6 +523,46 @@ impl ToolCard for TerminalToolCard {
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.when(!self.command_finished, |header| {
|
||||
header.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(tool_failed || command_failed, |header| {
|
||||
header.child(
|
||||
div()
|
||||
.id(("terminal-tool-error-code-indicator", self.entity_id))
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.when(command_failed && self.exit_status.is_some(), |this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Exited with code {}",
|
||||
self.exit_status
|
||||
.and_then(|status| status.code())
|
||||
.unwrap_or(-1),
|
||||
)))
|
||||
})
|
||||
.when(
|
||||
!command_failed && tool_failed && status.error().is_some(),
|
||||
|this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Error: {}",
|
||||
status.error().unwrap(),
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(self.was_content_truncated, |header| {
|
||||
let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||
"Output exceeded terminal max lines and was \
|
||||
@@ -555,34 +600,6 @@ impl ToolCard for TerminalToolCard {
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when(tool_failed || command_failed, |header| {
|
||||
header.child(
|
||||
div()
|
||||
.id(("terminal-tool-error-code-indicator", self.entity_id))
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.when(command_failed && self.exit_status.is_some(), |this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Exited with code {}",
|
||||
self.exit_status
|
||||
.and_then(|status| status.code())
|
||||
.unwrap_or(-1),
|
||||
)))
|
||||
})
|
||||
.when(
|
||||
!command_failed && tool_failed && status.error().is_some(),
|
||||
|this| {
|
||||
this.tooltip(Tooltip::text(format!(
|
||||
"Error: {}",
|
||||
status.error().unwrap(),
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(!self.finished_with_empty_output, |header| {
|
||||
header.child(
|
||||
Disclosure::new(
|
||||
@@ -634,6 +651,7 @@ impl ToolCard for TerminalToolCard {
|
||||
div()
|
||||
.pt_2()
|
||||
.border_t_1()
|
||||
.when(tool_failed || command_failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
|
||||
@@ -26,7 +26,7 @@ CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"
|
||||
|
||||
CREATE TABLE "access_tokens" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER REFERENCES users (id),
|
||||
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
|
||||
"impersonated_user_id" INTEGER REFERENCES users (id),
|
||||
"hash" VARCHAR(128)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE access_tokens DROP CONSTRAINT access_tokens_user_id_fkey;
|
||||
ALTER TABLE access_tokens ADD CONSTRAINT access_tokens_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
@@ -44,3 +44,53 @@ async fn test_accepted_tos(db: &Arc<Database>) {
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
assert!(user.accepted_tos_at.is_none());
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_destroy_user_cascade_deletes_access_tokens,
|
||||
test_destroy_user_cascade_deletes_access_tokens_postgres,
|
||||
test_destroy_user_cascade_deletes_access_tokens_sqlite
|
||||
);
|
||||
|
||||
async fn test_destroy_user_cascade_deletes_access_tokens(db: &Arc<Database>) {
|
||||
let user_id = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
Some("user1"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".to_string(),
|
||||
github_user_id: 12345,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user = db.get_user_by_id(user_id).await.unwrap();
|
||||
assert!(user.is_some());
|
||||
|
||||
let token_1_id = db
|
||||
.create_access_token(user_id, None, "token-1", 10)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_2_id = db
|
||||
.create_access_token(user_id, None, "token-2", 10)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_1 = db.get_access_token(token_1_id).await;
|
||||
let token_2 = db.get_access_token(token_2_id).await;
|
||||
assert!(token_1.is_ok());
|
||||
assert!(token_2.is_ok());
|
||||
|
||||
db.destroy_user(user_id).await.unwrap();
|
||||
|
||||
let user = db.get_user_by_id(user_id).await.unwrap();
|
||||
assert!(user.is_none());
|
||||
|
||||
let token_1 = db.get_access_token(token_1_id).await;
|
||||
let token_2 = db.get_access_token(token_2_id).await;
|
||||
assert!(token_1.is_err());
|
||||
assert!(token_2.is_err());
|
||||
}
|
||||
|
||||
@@ -528,6 +528,7 @@ impl CopilotChat {
|
||||
|
||||
pub async fn stream_completion(
|
||||
request: Request,
|
||||
is_user_initiated: bool,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let this = cx
|
||||
@@ -562,7 +563,14 @@ impl CopilotChat {
|
||||
};
|
||||
|
||||
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
|
||||
stream_completion(client.clone(), token.api_key, api_url.into(), request).await
|
||||
stream_completion(
|
||||
client.clone(),
|
||||
token.api_key,
|
||||
api_url.into(),
|
||||
request,
|
||||
is_user_initiated,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn set_configuration(
|
||||
@@ -697,6 +705,7 @@ async fn stream_completion(
|
||||
api_key: String,
|
||||
completion_url: Arc<str>,
|
||||
request: Request,
|
||||
is_user_initiated: bool,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let is_vision_request = request.messages.iter().any(|message| match message {
|
||||
ChatMessage::User { content }
|
||||
@@ -707,6 +716,8 @@ async fn stream_completion(
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let request_initiator = if is_user_initiated { "user" } else { "agent" };
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(completion_url.as_ref())
|
||||
@@ -719,7 +730,8 @@ async fn stream_completion(
|
||||
)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("X-Initiator", request_initiator);
|
||||
|
||||
if is_vision_request {
|
||||
request_builder =
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
adapters::DebugAdapterBinary,
|
||||
transport::{IoKind, LogKind, TransportDelegate},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use dap_types::{
|
||||
messages::{Message, Response},
|
||||
requests::Request,
|
||||
@@ -108,7 +108,11 @@ impl DebugAdapterClient {
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
self.transport_delegate
|
||||
.add_pending_request(sequence_id, callback_tx);
|
||||
.pending_requests
|
||||
.lock()
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.insert(sequence_id, callback_tx);
|
||||
|
||||
log::debug!(
|
||||
"Client {} send `{}` request with sequence_id: {}",
|
||||
|
||||
@@ -49,7 +49,6 @@ pub enum IoKind {
|
||||
StdErr,
|
||||
}
|
||||
|
||||
type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
|
||||
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
|
||||
|
||||
pub trait Transport: Send + Sync {
|
||||
@@ -93,18 +92,14 @@ async fn start(
|
||||
|
||||
pub(crate) struct TransportDelegate {
|
||||
log_handlers: LogHandlers,
|
||||
pub(crate) pending_requests: Requests,
|
||||
// TODO this should really be some kind of associative channel
|
||||
pub(crate) pending_requests:
|
||||
Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
pub(crate) transport: Mutex<Box<dyn Transport>>,
|
||||
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
|
||||
tasks: Mutex<Vec<Task<()>>>,
|
||||
}
|
||||
|
||||
impl Drop for TransportDelegate {
|
||||
fn drop(&mut self) {
|
||||
self.transport.lock().kill()
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportDelegate {
|
||||
pub(crate) async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result<Self> {
|
||||
let log_handlers: LogHandlers = Default::default();
|
||||
@@ -113,7 +108,7 @@ impl TransportDelegate {
|
||||
transport: Mutex::new(transport),
|
||||
log_handlers,
|
||||
server_tx: Default::default(),
|
||||
pending_requests: Default::default(),
|
||||
pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))),
|
||||
tasks: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -154,16 +149,26 @@ impl TransportDelegate {
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
pending_requests.lock().drain().for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
pending_requests
|
||||
.lock()
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
pending_requests.lock().drain().for_each(|(_, request)| {
|
||||
request.send(Err(e.cloned())).ok();
|
||||
});
|
||||
pending_requests
|
||||
.lock()
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request.send(Err(e.cloned())).ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -188,15 +193,6 @@ impl TransportDelegate {
|
||||
self.transport.lock().tcp_arguments()
|
||||
}
|
||||
|
||||
pub(crate) fn add_pending_request(
|
||||
&self,
|
||||
sequence_id: u64,
|
||||
request: oneshot::Sender<Result<Response>>,
|
||||
) {
|
||||
let mut pending_requests = self.pending_requests.lock();
|
||||
pending_requests.insert(sequence_id, request);
|
||||
}
|
||||
|
||||
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
|
||||
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
|
||||
server_tx.send(message).await.context("sending message")
|
||||
@@ -290,7 +286,7 @@ impl TransportDelegate {
|
||||
async fn recv_from_server<Stdout>(
|
||||
server_stdout: Stdout,
|
||||
mut message_handler: DapMessageHandler,
|
||||
pending_requests: Requests,
|
||||
pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -300,16 +296,21 @@ impl TransportDelegate {
|
||||
let mut reader = BufReader::new(server_stdout);
|
||||
|
||||
let result = loop {
|
||||
match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
|
||||
.await
|
||||
{
|
||||
let result =
|
||||
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
|
||||
.await;
|
||||
match result {
|
||||
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
return Ok(());
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Ok(Message::Response(res))) => {
|
||||
let tx = pending_requests.lock().remove(&res.request_seq);
|
||||
let tx = pending_requests
|
||||
.lock()
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.remove(&res.request_seq);
|
||||
if let Some(tx) = tx {
|
||||
if let Err(e) = tx.send(Self::process_response(res)) {
|
||||
log::trace!("Did not send response `{:?}` for a cancelled", e);
|
||||
|
||||
@@ -2,7 +2,6 @@ mod codelldb;
|
||||
mod gdb;
|
||||
mod go;
|
||||
mod javascript;
|
||||
mod php;
|
||||
mod python;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -22,7 +21,6 @@ use gdb::GdbDebugAdapter;
|
||||
use go::GoDebugAdapter;
|
||||
use gpui::{App, BorrowAppContext};
|
||||
use javascript::JsDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use serde_json::json;
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
@@ -31,7 +29,6 @@ pub fn init(cx: &mut App) {
|
||||
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
|
||||
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use adapters::latest_github_release;
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::{ResultExt, maybe};
|
||||
|
||||
@@ -70,6 +71,8 @@ impl JsDebugAdapter {
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut envs = HashMap::default();
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
maybe!({
|
||||
@@ -110,6 +113,12 @@ impl JsDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(env) = configuration.get("env").cloned() {
|
||||
if let Ok(env) = serde_json::from_value(env) {
|
||||
envs = env;
|
||||
}
|
||||
}
|
||||
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
@@ -158,7 +167,7 @@ impl JsDebugAdapter {
|
||||
),
|
||||
arguments,
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
envs,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
@@ -245,7 +254,7 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge", "node-terminal"],
|
||||
"description": "The type of debug session",
|
||||
"default": "pwa-node"
|
||||
},
|
||||
@@ -379,10 +388,6 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["program"] },
|
||||
{ "required": ["url"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
use adapters::latest_github_release;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::bail;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::StartDebuggingRequestArgumentsRequest;
|
||||
use dap::adapters::{DebugTaskDefinition, TcpArguments};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PhpDebugAdapter {
|
||||
checked: OnceLock<()>,
|
||||
}
|
||||
|
||||
impl PhpDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "PHP";
|
||||
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
|
||||
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching {asset_name:?}"))?
|
||||
.browser_download_url
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
user_args: Option<Vec<String>>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path
|
||||
} else {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
|
||||
|
||||
let file_name_prefix = format!("{}_", self.name());
|
||||
|
||||
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
|
||||
file_name.starts_with(&file_name_prefix)
|
||||
})
|
||||
.await
|
||||
.context("Couldn't find PHP dap directory")?
|
||||
};
|
||||
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(obj) = configuration.as_object_mut() {
|
||||
obj.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
let arguments = if let Some(mut args) = user_args {
|
||||
args.insert(
|
||||
0,
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
args
|
||||
} else {
|
||||
vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
format!("--server={}", port),
|
||||
]
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(
|
||||
delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
arguments,
|
||||
connection: Some(TcpArguments {
|
||||
port,
|
||||
host,
|
||||
timeout,
|
||||
}),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)
|
||||
.await?,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PhpDebugAdapter {
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"enum": ["launch"],
|
||||
"description": "The request type for the PHP debug adapter, always \"launch\"",
|
||||
"default": "launch"
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string",
|
||||
"description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
|
||||
"default": 9003
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The PHP script to debug (typically a path to a file)",
|
||||
"default": "${file}"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Working directory for the debugged program"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Command line arguments to pass to the program"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Environment variables to pass to the program",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to break at the beginning of the script",
|
||||
"default": false
|
||||
},
|
||||
"pathMappings": {
|
||||
"type": "object",
|
||||
"description": "A mapping of server paths to local paths.",
|
||||
},
|
||||
"log": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to log all communication between editor and the adapter to the debug console",
|
||||
"default": false
|
||||
},
|
||||
"ignore": {
|
||||
"type": "array",
|
||||
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ignoreExceptions": {
|
||||
"type": "array",
|
||||
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"skipFiles": {
|
||||
"type": "array",
|
||||
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"skipEntryPaths": {
|
||||
"type": "array",
|
||||
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"maxConnections": {
|
||||
"type": "integer",
|
||||
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
|
||||
"default": 1
|
||||
},
|
||||
"proxy": {
|
||||
"type": "object",
|
||||
"description": "DBGp Proxy settings",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"type": "boolean",
|
||||
"description": "To enable proxy registration",
|
||||
"default": false
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port where the adapter will register with the proxy",
|
||||
"default": 9001
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "A unique key that allows the proxy to match requests to your editor",
|
||||
"default": "vsc"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
|
||||
"default": 3000
|
||||
},
|
||||
"allowMultipleSessions": {
|
||||
"type": "boolean",
|
||||
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"xdebugSettings": {
|
||||
"type": "object",
|
||||
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
|
||||
"properties": {
|
||||
"max_children": {
|
||||
"type": "integer",
|
||||
"description": "Max number of array or object children to initially retrieve"
|
||||
},
|
||||
"max_data": {
|
||||
"type": "integer",
|
||||
"description": "Max amount of variable data to initially retrieve"
|
||||
},
|
||||
"max_depth": {
|
||||
"type": "integer",
|
||||
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
|
||||
},
|
||||
"show_hidden": {
|
||||
"type": "integer",
|
||||
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
|
||||
"enum": [0, 1]
|
||||
},
|
||||
"breakpoint_include_return_value": {
|
||||
"type": "boolean",
|
||||
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xdebugCloudToken": {
|
||||
"type": "string",
|
||||
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
|
||||
},
|
||||
"stream": {
|
||||
"type": "object",
|
||||
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
|
||||
"properties": {
|
||||
"stdout": {
|
||||
"type": "integer",
|
||||
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
|
||||
"enum": [0, 1, 2],
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["request", "program"]
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn adapter_language_name(&self) -> Option<LanguageName> {
|
||||
Some(SharedString::new_static("PHP").into())
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let obj = match &zed_scenario.request {
|
||||
dap::DebugRequest::Attach(_) => {
|
||||
bail!("Php adapter doesn't support attaching")
|
||||
}
|
||||
dap::DebugRequest::Launch(launch_config) => json!({
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args,
|
||||
"env": launch_config.env_json(),
|
||||
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
build: None,
|
||||
config: obj,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
user_args: Option<Vec<String>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if self.checked.set(()).is_ok() {
|
||||
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.get_installed_binary(
|
||||
delegate,
|
||||
&task_definition,
|
||||
user_installed_path,
|
||||
user_args,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
Err(anyhow::anyhow!("Not implemented"))
|
||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
self.extension
|
||||
.run_dap_locator(self.locator_name.as_ref().to_owned(), build_config)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
@@ -60,6 +61,7 @@ task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
telemetry.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
|
||||
@@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let candidate = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
@@ -229,30 +229,44 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(panel) = workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if secondary {
|
||||
// let Some(id) = worktree_id else { return };
|
||||
// cx.spawn_in(window, async move |_, cx| {
|
||||
// panel
|
||||
// .update_in(cx, |debug_panel, window, cx| {
|
||||
// debug_panel.save_scenario(&debug_scenario, id, window, cx)
|
||||
// })?
|
||||
// .await?;
|
||||
// anyhow::Ok(())
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
}
|
||||
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
|
||||
registry.adapter(&self.definition.adapter)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let definition = self.definition.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(panel) = panel {
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.start_session(scenario, Default::default(), None, None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.start_session(scenario, Default::default(), None, None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
|
||||
@@ -16,16 +16,18 @@ use dap::{
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use dap::{DapRegistry, StartDebuggingRequestArguments};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
|
||||
WeakEntity, anchored, deferred,
|
||||
};
|
||||
use text::ToPoint as _;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
@@ -33,10 +35,11 @@ use std::sync::{Arc, LazyLock};
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use tree_sitter::{Query, StreamingIterator as _};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::maybe;
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::item::SaveOptions;
|
||||
use workspace::{
|
||||
Pane, Workspace,
|
||||
Item, Pane, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
};
|
||||
use zed_actions::ToggleFocus;
|
||||
@@ -363,11 +366,17 @@ impl DebugPanel {
|
||||
let label = curr_session.read(cx).label().clone();
|
||||
let adapter = curr_session.read(cx).adapter().clone();
|
||||
let binary = curr_session.read(cx).binary().cloned().unwrap();
|
||||
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
|
||||
let task_context = curr_session.read(cx).task_context().clone();
|
||||
|
||||
let curr_session_id = curr_session.read(cx).session_id();
|
||||
self.sessions
|
||||
.retain(|session| session.read(cx).session_id(cx) != curr_session_id);
|
||||
let task = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(curr_session_id, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
task.await;
|
||||
task.await.log_err();
|
||||
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session = dap_store.new_session(label, adapter, task_context, None, cx);
|
||||
@@ -982,13 +991,90 @@ impl DebugPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn save_scenario(
|
||||
pub(crate) fn go_to_scenario_definition(
|
||||
&self,
|
||||
scenario: &DebugScenario,
|
||||
kind: TaskSourceKind,
|
||||
scenario: DebugScenario,
|
||||
worktree_id: WorktreeId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ProjectPath>> {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let project_path = match kind {
|
||||
TaskSourceKind::AbsPath { abs_path, .. } => {
|
||||
let Some(project_path) = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(&abs_path, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no abs path")));
|
||||
};
|
||||
|
||||
project_path
|
||||
}
|
||||
TaskSourceKind::Worktree {
|
||||
id,
|
||||
directory_in_worktree: dir,
|
||||
..
|
||||
} => {
|
||||
let relative_path = if dir.ends_with(".vscode") {
|
||||
dir.join("launch.json")
|
||||
} else {
|
||||
dir.join("debug.json")
|
||||
};
|
||||
ProjectPath {
|
||||
worktree_id: id,
|
||||
path: Arc::from(relative_path),
|
||||
}
|
||||
}
|
||||
_ => return self.save_scenario(scenario, worktree_id, window, cx),
|
||||
};
|
||||
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let editor = editor.await?;
|
||||
let editor = cx
|
||||
.update(|_, cx| editor.act_as::<Editor>(cx))?
|
||||
.context("expected editor")?;
|
||||
|
||||
// unfortunately debug tasks don't have an easy way to globally
|
||||
// identify them. to jump to the one that you just created or an
|
||||
// old one that you're choosing to edit we use a heuristic of searching for a line with `label: <your label>` from the end rather than the start so we bias towards more renctly
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
|
||||
if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
|
||||
Some(row)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(row) = row {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
text::Point::new(row as u32, 4),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn save_scenario(
|
||||
&self,
|
||||
scenario: DebugScenario,
|
||||
worktree_id: WorktreeId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let this = cx.weak_entity();
|
||||
let project = self.project.clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
@@ -1021,47 +1107,7 @@ impl DebugPanel {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut content = fs.load(path).await?;
|
||||
let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
|
||||
.lines()
|
||||
.map(|l| format!(" {l}"))
|
||||
.join("\n");
|
||||
|
||||
static ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
Query::new(
|
||||
&tree_sitter_json::LANGUAGE.into(),
|
||||
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
|
||||
)
|
||||
.expect("Failed to create ARRAY_QUERY")
|
||||
});
|
||||
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter_json::LANGUAGE.into())
|
||||
.unwrap();
|
||||
let mut cursor = tree_sitter::QueryCursor::new();
|
||||
let syntax_tree = parser.parse(&content, None).unwrap();
|
||||
let mut matches =
|
||||
cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes());
|
||||
|
||||
// we don't have `.last()` since it's a lending iterator, so loop over
|
||||
// the whole thing to find the last one
|
||||
let mut last_offset = None;
|
||||
while let Some(mat) = matches.next() {
|
||||
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
|
||||
last_offset = Some(pos)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = last_offset {
|
||||
content.insert_str(pos, &new_scenario);
|
||||
content.insert_str(pos, ",\n");
|
||||
}
|
||||
|
||||
fs.write(path, content.as_bytes()).await?;
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project_path = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
@@ -1069,12 +1115,113 @@ impl DebugPanel {
|
||||
.context(
|
||||
"Couldn't get project path for .zed/debug.json in active worktree",
|
||||
)
|
||||
})?
|
||||
})??;
|
||||
|
||||
let editor = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
let editor = cx
|
||||
.update(|_, cx| editor.act_as::<Editor>(cx))?
|
||||
.context("expected editor")?;
|
||||
|
||||
let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
|
||||
.lines()
|
||||
.map(|l| format!(" {l}"))
|
||||
.join("\n");
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
|
||||
})??
|
||||
.await
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
}
|
||||
|
||||
pub fn insert_task_into_editor(
|
||||
editor: &mut Editor,
|
||||
new_scenario: String,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
Query::new(
|
||||
&tree_sitter_json::LANGUAGE.into(),
|
||||
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
|
||||
)
|
||||
.expect("Failed to create LAST_ITEM_QUERY")
|
||||
});
|
||||
static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
Query::new(
|
||||
&tree_sitter_json::LANGUAGE.into(),
|
||||
"(document (array) @array)",
|
||||
)
|
||||
.expect("Failed to create EMPTY_ARRAY_QUERY")
|
||||
});
|
||||
|
||||
let content = editor.text(cx);
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
|
||||
let mut cursor = tree_sitter::QueryCursor::new();
|
||||
let syntax_tree = parser
|
||||
.parse(&content, None)
|
||||
.context("could not parse debug.json")?;
|
||||
let mut matches = cursor.matches(
|
||||
&LAST_ITEM_QUERY,
|
||||
syntax_tree.root_node(),
|
||||
content.as_bytes(),
|
||||
);
|
||||
|
||||
let mut last_offset = None;
|
||||
while let Some(mat) = matches.next() {
|
||||
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
|
||||
last_offset = Some(pos)
|
||||
}
|
||||
}
|
||||
let mut edits = Vec::new();
|
||||
let mut cursor_position = 0;
|
||||
|
||||
if let Some(pos) = last_offset {
|
||||
edits.push((pos..pos, format!(",\n{new_scenario}")));
|
||||
cursor_position = pos + ",\n ".len();
|
||||
} else {
|
||||
let mut matches = cursor.matches(
|
||||
&EMPTY_ARRAY_QUERY,
|
||||
syntax_tree.root_node(),
|
||||
content.as_bytes(),
|
||||
);
|
||||
|
||||
if let Some(mat) = matches.next() {
|
||||
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
|
||||
edits.push((pos..pos, format!("\n{new_scenario}\n")));
|
||||
cursor_position = pos + "\n ".len();
|
||||
}
|
||||
} else {
|
||||
edits.push((0..0, format!("[\n{}\n]", new_scenario)));
|
||||
cursor_position = "[\n ".len();
|
||||
}
|
||||
}
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.edit(edits, cx);
|
||||
let snapshot = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.snapshot();
|
||||
let point = cursor_position.to_point(&snapshot);
|
||||
editor.go_to_singleton_buffer_point(point, window, cx);
|
||||
});
|
||||
Ok(editor.save(SaveOptions::default(), project, window, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
@@ -1298,9 +1445,7 @@ impl Panel for DebugPanel {
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_sessions = self.sessions.len() > 0;
|
||||
let this = cx.weak_entity();
|
||||
debug_assert_eq!(has_sessions, self.active_session.is_some());
|
||||
|
||||
if self
|
||||
.active_session
|
||||
@@ -1487,8 +1632,8 @@ impl Render for DebugPanel {
|
||||
}))
|
||||
})
|
||||
.map(|this| {
|
||||
if has_sessions {
|
||||
this.children(self.active_session.clone())
|
||||
if let Some(active_session) = self.active_session.clone() {
|
||||
this.child(active_session)
|
||||
} else {
|
||||
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
|
||||
let welcome_experience = v_flex()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use anyhow::bail;
|
||||
use anyhow::{Context as _, bail};
|
||||
use collections::{FxHashMap, HashMap};
|
||||
use language::LanguageRegistry;
|
||||
use paths::local_debug_file_relative_path;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
use tasks_ui::{TaskOverrides, TasksModal};
|
||||
@@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
|
||||
KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{
|
||||
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
|
||||
};
|
||||
use settings::{Settings, initial_local_debug_tasks_content};
|
||||
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu,
|
||||
FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem,
|
||||
ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip,
|
||||
Window, div, prelude::*, px, relative, rems,
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
|
||||
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
|
||||
h_flex, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, pane};
|
||||
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
|
||||
#[allow(unused)]
|
||||
enum SaveScenarioState {
|
||||
Saving,
|
||||
Saved((ProjectPath, SharedString)),
|
||||
Failed(SharedString),
|
||||
}
|
||||
|
||||
pub(super) struct NewProcessModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
@@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
save_scenario_state: Option<SaveScenarioState>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
||||
@@ -268,7 +257,6 @@ impl NewProcessModal {
|
||||
mode,
|
||||
debug_panel: debug_panel.downgrade(),
|
||||
workspace: workspace_handle,
|
||||
save_scenario_state: None,
|
||||
_subscriptions,
|
||||
}
|
||||
});
|
||||
@@ -420,63 +408,29 @@ impl NewProcessModal {
|
||||
self.debug_picker.read(cx).delegate.task_contexts.clone()
|
||||
}
|
||||
|
||||
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task_contents = self.task_contexts(cx);
|
||||
pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task_contexts = self.task_contexts(cx);
|
||||
let Some(adapter) = self.debugger.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let scenario = self.debug_scenario(&adapter, cx);
|
||||
|
||||
self.save_scenario_state = Some(SaveScenarioState::Saving);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Some((scenario, worktree_id)) = scenario
|
||||
.await
|
||||
.zip(task_contents.and_then(|tcx| tcx.worktree()))
|
||||
else {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Failed(
|
||||
"Couldn't get scenario or task contents".into(),
|
||||
))
|
||||
let scenario = scenario.await.context("no scenario to save")?;
|
||||
let worktree_id = task_contexts
|
||||
.context("no task contexts")?
|
||||
.worktree()
|
||||
.context("no active worktree")?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.debug_panel.update(cx, |panel, cx| {
|
||||
panel.save_scenario(scenario, worktree_id, window, cx)
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(save_scenario) = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.save_scenario(&scenario, worktree_id, window, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let res = save_scenario.await;
|
||||
|
||||
this.update(cx, |this, _| match res {
|
||||
Ok(saved_file) => {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Saved((
|
||||
saved_file,
|
||||
scenario.label.clone(),
|
||||
)))
|
||||
}
|
||||
Err(error) => {
|
||||
this.save_scenario_state =
|
||||
Some(SaveScenarioState::Failed(error.to_string().into()))
|
||||
}
|
||||
})??
|
||||
.await?;
|
||||
this.update_in(cx, |_, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
this.update(cx, |this, _| this.save_scenario_state.take())
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
.detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None);
|
||||
}
|
||||
|
||||
fn adapter_drop_down_menu(
|
||||
@@ -544,70 +498,6 @@ impl NewProcessModal {
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
|
||||
let this = cx.entity();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let worktree_id = this.update(cx, |this, cx| {
|
||||
let tcx = this.task_contexts(cx);
|
||||
tcx?.worktree()
|
||||
})?;
|
||||
|
||||
let Some(worktree_id) = worktree_id else {
|
||||
let _ = cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
"Cannot open debug.json",
|
||||
Some("You must have at least one project open"),
|
||||
&[PromptButton::ok("Ok")],
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let editor = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: local_debug_file_relative_path().into(),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
if let Some(editor) = editor.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
if let Some(singleton) = buffer.as_singleton() {
|
||||
singleton.update(cx, |buffer, cx| {
|
||||
if buffer.is_empty() {
|
||||
buffer.edit(
|
||||
[(0..0, initial_local_debug_tasks_content())],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
|
||||
@@ -812,39 +702,21 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => el.child(
|
||||
container
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_sm(cx)
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.child(
|
||||
InteractiveText::new(
|
||||
"open-debug-json",
|
||||
StyledText::new(
|
||||
"Open .zed/debug.json for advanced configuration.",
|
||||
)
|
||||
.with_highlights([(
|
||||
5..20,
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.0),
|
||||
color: None,
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
)
|
||||
.on_click(
|
||||
vec![5..20],
|
||||
{
|
||||
let this = cx.entity();
|
||||
move |_, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.open_debug_json(window, cx);
|
||||
})
|
||||
}
|
||||
},
|
||||
h_flex().child(
|
||||
Button::new("edit-custom-debug", "Edit in debug.json")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_debug_scenario(window, cx);
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
.is_empty(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
@@ -862,29 +734,48 @@ impl Render for NewProcessModal {
|
||||
),
|
||||
),
|
||||
),
|
||||
NewProcessMode::Attach => el.child(
|
||||
NewProcessMode::Attach => el.child({
|
||||
let disabled = self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0;
|
||||
let secondary_action = menu::SecondaryConfirm.boxed_clone();
|
||||
container
|
||||
.child(div().child(self.adapter_drop_down_menu(window, cx)))
|
||||
.child(div().children(
|
||||
KeyBinding::for_action(&*secondary_action, window, cx).map(
|
||||
|keybind| {
|
||||
Button::new("edit-attach-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
secondary_action.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.disabled(disabled)
|
||||
},
|
||||
),
|
||||
))
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0,
|
||||
h_flex()
|
||||
.child(div().child(self.adapter_drop_down_menu(window, cx)))
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(disabled),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
NewProcessMode::Debug => el,
|
||||
NewProcessMode::Task => el,
|
||||
}
|
||||
@@ -1048,25 +939,6 @@ impl ConfigureMode {
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"debugger-save-to-debug-json",
|
||||
Label::new("Save to debug.json")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
self.save_to_debug_json,
|
||||
{
|
||||
let this = cx.weak_entity();
|
||||
move |state, _, cx| {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_to_debug_json = *state;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_input(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let text = self.prompt.clone();
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
@@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let task = task::TaskTemplate {
|
||||
label: "one-off".to_owned(),
|
||||
label: "one-off".to_owned(), // TODO: rename using command as label
|
||||
env,
|
||||
command: program,
|
||||
args,
|
||||
@@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
|
||||
.background_spawn(async move {
|
||||
for locator in locators {
|
||||
if let Some(scenario) =
|
||||
locator.1.create_scenario(&task, "one-off", &adapter).await
|
||||
// TODO: use a more informative label than "one-off"
|
||||
locator
|
||||
.1
|
||||
.create_scenario(&task, &task.label, &adapter)
|
||||
.await
|
||||
{
|
||||
return Some(scenario);
|
||||
}
|
||||
@@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<picker::Picker<Self>>,
|
||||
) {
|
||||
let debug_scenario = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
|
||||
|
||||
let Some((_, debug_scenario, context)) = debug_scenario else {
|
||||
let Some((kind, debug_scenario, context)) = debug_scenario else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
|
||||
});
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
active_buffer: _,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if secondary {
|
||||
let Some(kind) = kind else { return };
|
||||
let Some(id) = worktree_id else { return };
|
||||
let debug_panel = self.debug_panel.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
debug_panel
|
||||
.update_in(cx, |debug_panel, window, cx| {
|
||||
debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx)
|
||||
})?
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.ok();
|
||||
.detach();
|
||||
} else {
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
None,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
@@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
|
||||
let footer = h_flex()
|
||||
.w_full()
|
||||
.p_1p5()
|
||||
.justify_end()
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
// .child(
|
||||
// // TODO: add button to open selected task in debug.json
|
||||
// h_flex().into_any_element(),
|
||||
// )
|
||||
.children({
|
||||
let action = menu::SecondaryConfirm.boxed_clone();
|
||||
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("edit-debug-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
.map(|this| {
|
||||
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
|
||||
let action = picker::ConfirmInput {
|
||||
secondary: current_modifiers.secondary(),
|
||||
}
|
||||
.boxed_clone();
|
||||
let action = picker::ConfirmInput { secondary: false }.boxed_clone();
|
||||
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("launch-custom", "Launch Custom")
|
||||
.key_binding(keybind)
|
||||
@@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl NewProcessModal {
|
||||
pub(crate) fn set_configure(
|
||||
&mut self,
|
||||
program: impl AsRef<str>,
|
||||
cwd: impl AsRef<str>,
|
||||
stop_on_entry: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = NewProcessMode::Launch;
|
||||
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
|
||||
|
||||
self.configure_mode.update(cx, |configure, cx| {
|
||||
configure.program.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(program.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
configure.cwd.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(cwd.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
configure.stop_on_entry = match stop_on_entry {
|
||||
true => ToggleState::Selected,
|
||||
_ => ToggleState::Unselected,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,7 +973,7 @@ impl RunningState {
|
||||
|
||||
let task_with_shell = SpawnInTerminal {
|
||||
command_label,
|
||||
command,
|
||||
command: Some(command),
|
||||
args,
|
||||
..task.resolved.clone()
|
||||
};
|
||||
@@ -1085,19 +1085,6 @@ impl RunningState {
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| session.binary().unwrap().cwd.clone());
|
||||
|
||||
let mut args = request.args.clone();
|
||||
|
||||
// Handle special case for NodeJS debug adapter
|
||||
// If only the Node binary path is provided, we set the command to None
|
||||
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
|
||||
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
|
||||
// This allows the NodeJS debug client to attach correctly
|
||||
let command = if args.len() > 1 {
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut envs: HashMap<String, String> =
|
||||
self.session.read(cx).task_context().project_env.clone();
|
||||
if let Some(Value::Object(env)) = &request.env {
|
||||
@@ -1111,32 +1098,58 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let kind = if let Some(command) = command {
|
||||
let title = request.title.clone().unwrap_or(command.clone());
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: task::TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: task::RevealStrategy::NoFocus,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
})
|
||||
let mut args = request.args.clone();
|
||||
let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
|
||||
// Handle special case for NodeJS debug adapter
|
||||
// If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
|
||||
// we set the command to None
|
||||
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
|
||||
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
|
||||
// This allows the NodeJS debug client to attach correctly
|
||||
if args
|
||||
.iter()
|
||||
.filter(|arg| !arg.starts_with("--"))
|
||||
.collect::<Vec<_>>()
|
||||
.len()
|
||||
> 1
|
||||
{
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if args.len() > 0 {
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
|
||||
None
|
||||
};
|
||||
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let title = request
|
||||
.title
|
||||
.clone()
|
||||
.filter(|title| !title.is_empty())
|
||||
.or_else(|| command.clone())
|
||||
.unwrap_or_else(|| "Debug terminal".to_string());
|
||||
let kind = TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: task::TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: task::RevealStrategy::NoFocus,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let weak_project = project.downgrade();
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter};
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
@@ -16,6 +17,7 @@ use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
|
||||
dap_store::{DapStore, PersistedAdapterOptions},
|
||||
session::Session,
|
||||
},
|
||||
worktree_store::WorktreeStore,
|
||||
@@ -48,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind {
|
||||
pub(crate) struct BreakpointList {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
dap_store: Entity<DapStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
breakpoints: Vec<BreakpointEntry>,
|
||||
@@ -59,6 +62,7 @@ pub(crate) struct BreakpointList {
|
||||
selected_ix: Option<usize>,
|
||||
input: Entity<Editor>,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointList {
|
||||
@@ -85,24 +89,34 @@ impl BreakpointList {
|
||||
let project = project.read(cx);
|
||||
let breakpoint_store = project.breakpoint_store();
|
||||
let worktree_store = project.worktree_store();
|
||||
let dap_store = project.dap_store();
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
cx.new(|cx| Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
|
||||
cx.new(|cx| {
|
||||
let this = Self {
|
||||
breakpoint_store,
|
||||
dap_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
serialize_exception_breakpoints_task: None,
|
||||
};
|
||||
if let Some(name) = adapter_name {
|
||||
_ = this.deserialize_exception_breakpoints(name, cx);
|
||||
}
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
@@ -404,12 +418,8 @@ impl BreakpointList {
|
||||
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
if let Some(session) = &self.session {
|
||||
let id = exception_breakpoint.id.clone();
|
||||
session.update(cx, |session, cx| {
|
||||
session.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
}
|
||||
let id = exception_breakpoint.id.clone();
|
||||
self.toggle_exception_breakpoint(&id, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
@@ -480,6 +490,64 @@ impl BreakpointList {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
if let Some(session) = &self.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(EXCEPTION_SERIALIZATION_INTERVAL)
|
||||
.await;
|
||||
this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
|
||||
.await?;
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn kvp_key(adapter_name: &str) -> String {
|
||||
format!("debug_adapter_`{adapter_name}`_persistence")
|
||||
}
|
||||
fn serialize_exception_breakpoints(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
if let Some(session) = self.session.as_ref() {
|
||||
let key = {
|
||||
let session = session.read(cx);
|
||||
let name = session.adapter().0;
|
||||
Self::kvp_key(&name)
|
||||
};
|
||||
let settings = self.dap_store.update(cx, |this, cx| {
|
||||
this.sync_adapter_options(session, cx);
|
||||
});
|
||||
let value = serde_json::to_string(&settings);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
|
||||
} else {
|
||||
return Task::ready(Result::Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_exception_breakpoints(
|
||||
&self,
|
||||
adapter_name: DebugAdapterName,
|
||||
cx: &mut Context<Self>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
|
||||
return Ok(());
|
||||
};
|
||||
let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
|
||||
self.dap_store
|
||||
.update(cx, |this, _| this.set_adapter_options(adapter_name, value));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
@@ -988,12 +1056,7 @@ impl ExceptionBreakpoint {
|
||||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
if let Some(session) = &this.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::{
|
||||
use alacritty_terminal::vte::ansi;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use dap::{CompletionItem, CompletionItemType, OutputEvent};
|
||||
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -17,6 +17,7 @@ use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Completion, CompletionResponse,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
lsp_store::CompletionDocumentation,
|
||||
search_history::{SearchHistory, SearchHistoryCursor},
|
||||
};
|
||||
use settings::Settings;
|
||||
@@ -555,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
trigger_in_words: bool,
|
||||
menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let char = if let Some(char) = chars.next() {
|
||||
char
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
|
||||
return false;
|
||||
}
|
||||
|
||||
let classifier = snapshot.char_classifier_at(position).for_completion(true);
|
||||
if trigger_in_words && classifier.is_word(char) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.0
|
||||
.read_with(cx, |console, cx| {
|
||||
console
|
||||
@@ -596,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
variable_list.completion_variables(cx)
|
||||
}) {
|
||||
if let Some(evaluate_name) = &variable.evaluate_name {
|
||||
variables.insert(evaluate_name.clone(), variable.value.clone());
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
if variables
|
||||
.insert(evaluate_name.clone(), variable.value.clone())
|
||||
.is_none()
|
||||
{
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
variables.insert(variable.name.clone(), variable.value.clone());
|
||||
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
if variables
|
||||
.insert(variable.name.clone(), variable.value.clone())
|
||||
.is_none()
|
||||
{
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(variables, string_matches)
|
||||
@@ -656,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string, variable_value),
|
||||
text: string_match.string.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
|
||||
variable_value.into(),
|
||||
)),
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
@@ -675,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
const fn completion_type_score(completion_type: CompletionItemType) -> usize {
|
||||
match completion_type {
|
||||
CompletionItemType::Field | CompletionItemType::Property => 0,
|
||||
CompletionItemType::Variable | CompletionItemType::Value => 1,
|
||||
CompletionItemType::Method
|
||||
| CompletionItemType::Function
|
||||
| CompletionItemType::Constructor => 2,
|
||||
CompletionItemType::Class
|
||||
| CompletionItemType::Interface
|
||||
| CompletionItemType::Module => 3,
|
||||
_ => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
|
||||
completion_item.sort_text.clone().unwrap_or_else(|| {
|
||||
format!(
|
||||
"{:03}_{}",
|
||||
Self::completion_type_score(
|
||||
completion_item.type_.unwrap_or(CompletionItemType::Text)
|
||||
),
|
||||
completion_item.label.to_ascii_lowercase()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn client_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
@@ -699,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
let completions = completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let sort_text = Self::completion_item_sort_text(&completion);
|
||||
let new_text = completion
|
||||
.text
|
||||
.as_ref()
|
||||
@@ -731,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
documentation: completion.detail.map(|detail| {
|
||||
CompletionDocumentation::MultiLineMarkdown(detail.into())
|
||||
}),
|
||||
confirm: None,
|
||||
source: project::CompletionSource::BufferWord {
|
||||
word_range: buffer_position..language::Anchor::MAX,
|
||||
resolved: false,
|
||||
},
|
||||
source: project::CompletionSource::Dap { sort_text },
|
||||
insert_text_mode: None,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let session = start_debug_session_with(
|
||||
let _session = start_debug_session_with(
|
||||
&workspace,
|
||||
cx,
|
||||
DebugTaskDefinition {
|
||||
@@ -59,14 +59,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
|
||||
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use dap::DapRegistry;
|
||||
use editor::Editor;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use project::{FakeFs, Fs as _, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
|
||||
use text::Point;
|
||||
use util::path;
|
||||
|
||||
// use crate::new_process_modal::NewProcessMode;
|
||||
use crate::NewProcessMode;
|
||||
use crate::tests::{init_test, init_test_workspace};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -159,111 +161,127 @@ 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_process_modal::NewProcessModal::show(
|
||||
// workspace,
|
||||
// window,
|
||||
// NewProcessMode::Debug,
|
||||
// None,
|
||||
// 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_process_modal::NewProcessModal>(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_configure("/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_debug_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 editor = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// 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 debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist")
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("//"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
// pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
let expected_content = indoc::indoc! {r#"
|
||||
[
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "main (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/main",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
]"#};
|
||||
|
||||
// modal.update_in(cx, |modal, window, cx| {
|
||||
// modal.set_configure("/project/other", "/project", true, window, cx);
|
||||
// modal.save_scenario(window, cx);
|
||||
// });
|
||||
pretty_assertions::assert_eq!(expected_content, debug_json_content);
|
||||
|
||||
// cx.executor().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.selections.newest::<Point>(cx).head(),
|
||||
Point::new(5, 2)
|
||||
)
|
||||
});
|
||||
|
||||
// let debug_json_content = fs
|
||||
// .load(path!("/project/.zed/debug.json").as_ref())
|
||||
// .await
|
||||
// .expect("debug.json should exist after second save");
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_configure("/project/other", "/project", true, window, cx);
|
||||
modal.save_debug_scenario(window, cx);
|
||||
});
|
||||
|
||||
// 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": {}"#,
|
||||
// " }",
|
||||
// "]",
|
||||
// ];
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
|
||||
// pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
// }
|
||||
let expected_content = indoc::indoc! {r#"
|
||||
[
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "main (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/main",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
},
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "other (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/other",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
]"#};
|
||||
|
||||
let debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist")
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("//"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
pretty_assertions::assert_eq!(expected_content, debug_json_content);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
|
||||
@@ -272,7 +290,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
|
||||
let mut expected_adapters = vec![
|
||||
"CodeLLDB",
|
||||
"Debugpy",
|
||||
"PHP",
|
||||
"JavaScript",
|
||||
"Delve",
|
||||
"GDB",
|
||||
|
||||
@@ -635,6 +635,8 @@ actions!(
|
||||
SignatureHelpNext,
|
||||
/// Navigates to the previous signature in the signature help popup.
|
||||
SignatureHelpPrevious,
|
||||
/// Sorts selected lines by length.
|
||||
SortLinesByLength,
|
||||
/// Sorts selected lines case-insensitively.
|
||||
SortLinesCaseInsensitive,
|
||||
/// Sorts selected lines case-sensitively.
|
||||
|
||||
@@ -1083,11 +1083,10 @@ impl CompletionsMenu {
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source
|
||||
{
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
let sort_text = match &completion.source {
|
||||
CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
|
||||
CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let (sort_kind, sort_label) = completion.sort_key();
|
||||
|
||||
@@ -1066,7 +1066,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
|
||||
text_system.layout_line(&line, font_size, &runs)
|
||||
text_system.layout_line(&line, font_size, &runs, None)
|
||||
}
|
||||
|
||||
pub fn x_for_display_point(
|
||||
|
||||
@@ -571,7 +571,7 @@ impl Default for EditorStyle {
|
||||
// HACK: Status colors don't have a real default.
|
||||
// We should look into removing the status colors from the editor
|
||||
// style and retrieve them directly from the theme.
|
||||
status: StatusColors::dark(),
|
||||
status: StatusColors::default(),
|
||||
inlay_hints_style: HighlightStyle::default(),
|
||||
inline_completion_styles: InlineCompletionStyles {
|
||||
insertion: HighlightStyle::default(),
|
||||
@@ -10204,6 +10204,17 @@ impl Editor {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
|
||||
}
|
||||
|
||||
pub fn sort_lines_by_length(
|
||||
&mut self,
|
||||
_: &SortLinesByLength,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
lines.sort_by_key(|&line| line.chars().count())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sort_lines_case_insensitive(
|
||||
&mut self,
|
||||
_: &SortLinesCaseInsensitive,
|
||||
|
||||
@@ -4075,6 +4075,29 @@ async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppC
|
||||
Zˇ»
|
||||
"});
|
||||
|
||||
// Test sort_lines_by_length()
|
||||
//
|
||||
// Demonstrates:
|
||||
// - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
|
||||
// - sort is stable
|
||||
cx.set_state(indoc! {"
|
||||
«123
|
||||
æ
|
||||
12
|
||||
∞
|
||||
1
|
||||
æˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«æ
|
||||
∞
|
||||
1
|
||||
æ
|
||||
12
|
||||
123ˇ»
|
||||
"});
|
||||
|
||||
// Test reverse_lines()
|
||||
cx.set_state(indoc! {"
|
||||
«5
|
||||
@@ -22325,6 +22348,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
def f() -> list[str]:
|
||||
aˇ
|
||||
"});
|
||||
|
||||
// test does not outdent on typing : after case keyword
|
||||
cx.set_state(indoc! {"
|
||||
match 1:
|
||||
caseˇ
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
match 1:
|
||||
case:ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -225,6 +225,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::autoindent);
|
||||
register_action(editor, window, Editor::delete_line);
|
||||
register_action(editor, window, Editor::join_lines);
|
||||
register_action(editor, window, Editor::sort_lines_by_length);
|
||||
register_action(editor, window, Editor::sort_lines_case_sensitive);
|
||||
register_action(editor, window, Editor::sort_lines_case_insensitive);
|
||||
register_action(editor, window, Editor::reverse_lines);
|
||||
@@ -1610,6 +1611,7 @@ impl EditorElement {
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
None,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
@@ -2828,6 +2830,7 @@ impl EditorElement {
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let active_task_indicator_row =
|
||||
// TODO: add edit button on the right side of each row in the context menu
|
||||
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from,
|
||||
actions,
|
||||
@@ -3261,10 +3264,12 @@ impl EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let line =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(line.to_string().into(), font_size, &[run]);
|
||||
let line = window.text_system().shape_line(
|
||||
line.to_string().into(),
|
||||
font_size,
|
||||
&[run],
|
||||
None,
|
||||
);
|
||||
LineWithInvisibles {
|
||||
width: line.width,
|
||||
len: line.len,
|
||||
@@ -6886,6 +6891,7 @@ impl EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None,
|
||||
);
|
||||
|
||||
layout.width
|
||||
@@ -6914,6 +6920,7 @@ impl EditorElement {
|
||||
text,
|
||||
self.style.text.font_size.to_pixels(window.rem_size()),
|
||||
&[run],
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7182,10 +7189,12 @@ impl LineWithInvisibles {
|
||||
}]) {
|
||||
if let Some(replacement) = highlighted_chunk.replacement {
|
||||
if !line.is_empty() {
|
||||
let shaped_line =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(line.clone().into(), font_size, &styles);
|
||||
let shaped_line = window.text_system().shape_line(
|
||||
line.clone().into(),
|
||||
font_size,
|
||||
&styles,
|
||||
None,
|
||||
);
|
||||
width += shaped_line.width;
|
||||
len += shaped_line.len;
|
||||
fragments.push(LineFragment::Text(shaped_line));
|
||||
@@ -7205,6 +7214,7 @@ impl LineWithInvisibles {
|
||||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
None,
|
||||
);
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
@@ -7249,7 +7259,7 @@ impl LineWithInvisibles {
|
||||
};
|
||||
let line_layout = window
|
||||
.text_system()
|
||||
.shape_line(x, font_size, &[run])
|
||||
.shape_line(x, font_size, &[run], None)
|
||||
.with_len(highlighted_chunk.text.len());
|
||||
|
||||
width += line_layout.width;
|
||||
@@ -7264,6 +7274,7 @@ impl LineWithInvisibles {
|
||||
line.clone().into(),
|
||||
font_size,
|
||||
&styles,
|
||||
None,
|
||||
);
|
||||
width += shaped_line.width;
|
||||
len += shaped_line.len;
|
||||
@@ -7933,6 +7944,7 @@ impl Element for EditorElement {
|
||||
editor.last_bounds = Some(bounds);
|
||||
editor.gutter_dimensions = gutter_dimensions;
|
||||
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
|
||||
editor.set_visible_column_count(editor_content_width / em_advance);
|
||||
|
||||
if matches!(
|
||||
editor.mode,
|
||||
@@ -8438,6 +8450,7 @@ impl Element for EditorElement {
|
||||
scroll_width,
|
||||
em_advance,
|
||||
&line_layouts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
@@ -8592,6 +8605,7 @@ impl Element for EditorElement {
|
||||
scroll_width,
|
||||
em_advance,
|
||||
&line_layouts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
@@ -8829,6 +8843,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
);
|
||||
let space_invisible = window.text_system().shape_line(
|
||||
"•".into(),
|
||||
@@ -8841,6 +8856,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
);
|
||||
|
||||
let mode = snapshot.mode.clone();
|
||||
|
||||
@@ -381,10 +381,14 @@ fn show_hover(
|
||||
.anchor_after(local_diagnostic.range.end),
|
||||
};
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
|
||||
Some(DiagnosticPopover {
|
||||
local_diagnostic,
|
||||
markdown,
|
||||
border_color,
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
scroll_handle,
|
||||
background_color,
|
||||
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
|
||||
anchor,
|
||||
@@ -955,6 +959,8 @@ pub struct DiagnosticPopover {
|
||||
pub keyboard_grace: Rc<RefCell<bool>>,
|
||||
pub anchor: Anchor,
|
||||
_subscription: Subscription,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
pub scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl DiagnosticPopover {
|
||||
@@ -968,10 +974,7 @@ impl DiagnosticPopover {
|
||||
let this = cx.entity().downgrade();
|
||||
div()
|
||||
.id("diagnostic")
|
||||
.block()
|
||||
.max_h(max_size.height)
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.occlude()
|
||||
.elevation_2_borderless(cx)
|
||||
// Don't draw the background color if the theme
|
||||
// allows transparent surfaces.
|
||||
@@ -992,27 +995,72 @@ impl DiagnosticPopover {
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(window, cx),
|
||||
)
|
||||
.on_url_click(move |link, window, cx| {
|
||||
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
renderer.as_ref().open_link(this, link, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.bg(self.background_color)
|
||||
.border_1()
|
||||
.border_color(self.border_color)
|
||||
.rounded_lg(),
|
||||
.rounded_lg()
|
||||
.child(
|
||||
div()
|
||||
.id("diagnostic-content-container")
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(window, cx),
|
||||
)
|
||||
.on_url_click(
|
||||
move |link, window, cx| {
|
||||
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
renderer.as_ref().open_link(this, link, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(self.render_vertical_scrollbar(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("diagnostic-popover-vertical-scroll")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
|
||||
use core::fmt::Debug;
|
||||
use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px};
|
||||
use language::language_settings::{AllLanguageSettings, SoftWrap};
|
||||
use language::{Bias, Point};
|
||||
pub use scroll_amount::ScrollAmount;
|
||||
use settings::Settings;
|
||||
@@ -151,12 +152,16 @@ pub struct ScrollManager {
|
||||
pub(crate) vertical_scroll_margin: f32,
|
||||
anchor: ScrollAnchor,
|
||||
ongoing: OngoingScroll,
|
||||
/// The second element indicates whether the autoscroll request is local
|
||||
/// (true) or remote (false). Local requests are initiated by user actions,
|
||||
/// while remote requests come from external sources.
|
||||
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
active_scrollbar: Option<ActiveScrollbarState>,
|
||||
visible_line_count: Option<f32>,
|
||||
visible_column_count: Option<f32>,
|
||||
forbid_vertical_scroll: bool,
|
||||
minimap_thumb_state: Option<ScrollbarThumbState>,
|
||||
}
|
||||
@@ -173,6 +178,7 @@ impl ScrollManager {
|
||||
active_scrollbar: None,
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
visible_column_count: None,
|
||||
forbid_vertical_scroll: false,
|
||||
minimap_thumb_state: None,
|
||||
}
|
||||
@@ -210,7 +216,7 @@ impl ScrollManager {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: Anchor::min(),
|
||||
@@ -218,6 +224,22 @@ impl ScrollManager {
|
||||
},
|
||||
0,
|
||||
)
|
||||
} else if scroll_position.y <= 0. {
|
||||
let buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(DisplayRow(0), scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right);
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: anchor,
|
||||
offset: scroll_position.max(&gpui::Point::default()),
|
||||
},
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
let scroll_top = scroll_position.y;
|
||||
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
|
||||
@@ -242,8 +264,13 @@ impl ScrollManager {
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_top_buffer_point =
|
||||
DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let top_anchor = map
|
||||
.buffer_snapshot
|
||||
.anchor_at(scroll_top_buffer_point, Bias::Right);
|
||||
@@ -476,6 +503,10 @@ impl Editor {
|
||||
.map(|line_count| line_count as u32 - 1)
|
||||
}
|
||||
|
||||
pub fn visible_column_count(&self) -> Option<f32> {
|
||||
self.scroll_manager.visible_column_count
|
||||
}
|
||||
|
||||
pub(crate) fn set_visible_line_count(
|
||||
&mut self,
|
||||
lines: f32,
|
||||
@@ -497,6 +528,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_visible_column_count(&mut self, columns: f32) {
|
||||
self.scroll_manager.visible_column_count = Some(columns);
|
||||
}
|
||||
|
||||
pub fn apply_scroll_delta(
|
||||
&mut self,
|
||||
scroll_delta: gpui::Point<f32>,
|
||||
@@ -675,25 +710,48 @@ impl Editor {
|
||||
let Some(visible_line_count) = self.visible_line_count() else {
|
||||
return;
|
||||
};
|
||||
let Some(mut visible_column_count) = self.visible_column_count() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the user has a preferred line length, and has the editor
|
||||
// configured to wrap at the preferred line length, or bounded to it,
|
||||
// use that value over the visible column count. This was mostly done so
|
||||
// that tests could actually be written for vim's `z l`, `z h`, `z
|
||||
// shift-l` and `z shift-h` commands, as there wasn't a good way to
|
||||
// configure the editor to only display a certain number of columns. If
|
||||
// that ever happens, this could probably be removed.
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
if matches!(
|
||||
settings.defaults.soft_wrap,
|
||||
SoftWrap::PreferredLineLength | SoftWrap::Bounded
|
||||
) {
|
||||
if (settings.defaults.preferred_line_length as f32) < visible_column_count {
|
||||
visible_column_count = settings.defaults.preferred_line_length as f32;
|
||||
}
|
||||
}
|
||||
|
||||
// If the scroll position is currently at the left edge of the document
|
||||
// (x == 0.0) and the intent is to scroll right, the gutter's margin
|
||||
// should first be added to the current position, otherwise the cursor
|
||||
// will end at the column position minus the margin, which looks off.
|
||||
if current_position.x == 0.0 && amount.columns() > 0. {
|
||||
if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. {
|
||||
if let Some(last_position_map) = &self.last_position_map {
|
||||
current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
|
||||
}
|
||||
}
|
||||
let new_position =
|
||||
current_position + point(amount.columns(), amount.lines(visible_line_count));
|
||||
let new_position = current_position
|
||||
+ point(
|
||||
amount.columns(visible_column_count),
|
||||
amount.lines(visible_line_count),
|
||||
);
|
||||
self.set_scroll_position(new_position, window, cx);
|
||||
}
|
||||
|
||||
/// Returns an ordering. The newest selection is:
|
||||
/// Ordering::Equal => on screen
|
||||
/// Ordering::Less => above the screen
|
||||
/// Ordering::Greater => below the screen
|
||||
/// Ordering::Less => above or to the left of the screen
|
||||
/// Ordering::Greater => below or to the right of the screen
|
||||
pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
|
||||
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let newest_head = self
|
||||
@@ -711,8 +769,12 @@ impl Editor {
|
||||
return Ordering::Less;
|
||||
}
|
||||
|
||||
if let Some(visible_lines) = self.visible_line_count() {
|
||||
if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
|
||||
if let (Some(visible_lines), Some(visible_columns)) =
|
||||
(self.visible_line_count(), self.visible_column_count())
|
||||
{
|
||||
if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
|
||||
&& newest_head.column() <= screen_top.column() + visible_columns as u32
|
||||
{
|
||||
return Ordering::Equal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,12 +274,14 @@ impl Editor {
|
||||
start_row: DisplayRow,
|
||||
viewport_width: Pixels,
|
||||
scroll_width: Pixels,
|
||||
max_glyph_width: Pixels,
|
||||
em_advance: Pixels,
|
||||
layouts: &[LineWithInvisibles],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
|
||||
let mut target_left;
|
||||
let mut target_right;
|
||||
@@ -295,16 +297,17 @@ impl Editor {
|
||||
if head.row() >= start_row
|
||||
&& head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
|
||||
{
|
||||
let start_column = head.column().saturating_sub(3);
|
||||
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
|
||||
let start_column = head.column();
|
||||
let end_column = cmp::min(display_map.line_len(head.row()), head.column());
|
||||
target_left = target_left.min(
|
||||
layouts[head.row().minus(start_row) as usize]
|
||||
.x_for_index(start_column as usize),
|
||||
.x_for_index(start_column as usize)
|
||||
+ self.gutter_dimensions.margin,
|
||||
);
|
||||
target_right = target_right.max(
|
||||
layouts[head.row().minus(start_row) as usize]
|
||||
.x_for_index(end_column as usize)
|
||||
+ max_glyph_width,
|
||||
+ em_advance,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -319,14 +322,16 @@ impl Editor {
|
||||
return false;
|
||||
}
|
||||
|
||||
let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width;
|
||||
let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
|
||||
let scroll_right = scroll_left + viewport_width;
|
||||
|
||||
if target_left < scroll_left {
|
||||
self.scroll_manager.anchor.offset.x = target_left / max_glyph_width;
|
||||
scroll_position.x = target_left / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
} else if target_right > scroll_right {
|
||||
self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width;
|
||||
scroll_position.x = (target_right - viewport_width) / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -23,6 +23,8 @@ pub enum ScrollAmount {
|
||||
Page(f32),
|
||||
// Scroll N columns (positive is towards the right of the document)
|
||||
Column(f32),
|
||||
// Scroll N page width (positive is towards the right of the document)
|
||||
PageWidth(f32),
|
||||
}
|
||||
|
||||
impl ScrollAmount {
|
||||
@@ -37,14 +39,16 @@ impl ScrollAmount {
|
||||
(visible_line_count * count).trunc()
|
||||
}
|
||||
Self::Column(_count) => 0.0,
|
||||
Self::PageWidth(_count) => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> f32 {
|
||||
pub fn columns(&self, visible_column_count: f32) -> f32 {
|
||||
match self {
|
||||
Self::Line(_count) => 0.0,
|
||||
Self::Page(_count) => 0.0,
|
||||
Self::Column(count) => *count,
|
||||
Self::PageWidth(count) => (visible_column_count * count).trunc(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +62,7 @@ impl ScrollAmount {
|
||||
// so I'm leaving this at 0.0 for now to try and make it clear that
|
||||
// this should not have an impact on that?
|
||||
ScrollAmount::Column(_) => px(0.0),
|
||||
ScrollAmount::PageWidth(_) => px(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ impl Example for FileChangeNotificationExample {
|
||||
url: "https://github.com/octocat/hello-world".to_string(),
|
||||
revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(),
|
||||
language_server: None,
|
||||
max_assertions: Some(1),
|
||||
max_assertions: None,
|
||||
profile_id: AgentProfileId::default(),
|
||||
existing_thread_json: None,
|
||||
max_turns: Some(3),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
|
||||
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path,
|
||||
parse_wasm_extension_version,
|
||||
};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
@@ -99,12 +100,8 @@ impl ExtensionBuilder {
|
||||
}
|
||||
|
||||
for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters {
|
||||
let debug_adapter_relative_schema_path =
|
||||
meta.schema_path.clone().unwrap_or_else(|| {
|
||||
Path::new("debug_adapter_schemas")
|
||||
.join(Path::new(debug_adapter_name.as_ref()).with_extension("json"))
|
||||
});
|
||||
let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path);
|
||||
let debug_adapter_schema_path =
|
||||
extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta));
|
||||
|
||||
let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path)
|
||||
.with_context(|| {
|
||||
|
||||
@@ -130,6 +130,22 @@ impl ExtensionManifest {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn allow_remote_load(&self) -> bool {
|
||||
!self.language_servers.is_empty()
|
||||
|| !self.debug_adapters.is_empty()
|
||||
|| !self.debug_locators.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_debug_adapter_schema_path(
|
||||
adapter_name: &Arc<str>,
|
||||
meta: &DebugAdapterManifestEntry,
|
||||
) -> PathBuf {
|
||||
meta.schema_path.clone().unwrap_or_else(|| {
|
||||
Path::new("debug_adapter_schemas")
|
||||
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
|
||||
})
|
||||
}
|
||||
|
||||
/// A capability for an extension.
|
||||
@@ -320,6 +336,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_adapter_schema_path_with_schema_path() {
|
||||
let adapter_name = Arc::from("my_adapter");
|
||||
let entry = DebugAdapterManifestEntry {
|
||||
schema_path: Some(PathBuf::from("foo/bar")),
|
||||
};
|
||||
|
||||
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
|
||||
assert_eq!(path, PathBuf::from("foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_adapter_schema_path_without_schema_path() {
|
||||
let adapter_name = Arc::from("my_adapter");
|
||||
let entry = DebugAdapterManifestEntry { schema_path: None };
|
||||
|
||||
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
|
||||
assert_eq!(
|
||||
path,
|
||||
PathBuf::from("debug_adapter_schemas").join("my_adapter.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_exact_match() {
|
||||
let manifest = ExtensionManifest {
|
||||
|
||||
@@ -54,7 +54,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use url::Url;
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, paths::RemotePathBuf};
|
||||
use wasm_host::{
|
||||
WasmExtension, WasmHost,
|
||||
wit::{is_supported_wasm_api_version, wasm_api_version_range},
|
||||
@@ -1639,6 +1639,23 @@ impl ExtensionStore {
|
||||
}
|
||||
}
|
||||
|
||||
for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() {
|
||||
let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
|
||||
|
||||
if fs.is_file(&src_dir.join(schema_path)).await {
|
||||
match schema_path.parent() {
|
||||
Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
|
||||
None => {}
|
||||
}
|
||||
fs.copy_file(
|
||||
&src_dir.join(schema_path),
|
||||
&tmp_dir.join(schema_path),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -1653,7 +1670,7 @@ impl ExtensionStore {
|
||||
.extensions
|
||||
.iter()
|
||||
.filter_map(|(id, entry)| {
|
||||
if entry.manifest.language_servers.is_empty() {
|
||||
if !entry.manifest.allow_remote_load() {
|
||||
return None;
|
||||
}
|
||||
Some(proto::Extension {
|
||||
@@ -1672,6 +1689,7 @@ impl ExtensionStore {
|
||||
.request(proto::SyncExtensions { extensions })
|
||||
})?
|
||||
.await?;
|
||||
let path_style = client.read_with(cx, |client, _| client.path_style())?;
|
||||
|
||||
for missing_extension in response.missing_extensions.into_iter() {
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
@@ -1684,7 +1702,10 @@ impl ExtensionStore {
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
|
||||
let dest_dir = RemotePathBuf::new(
|
||||
PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
|
||||
path_style,
|
||||
);
|
||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||
|
||||
client
|
||||
@@ -1701,7 +1722,7 @@ impl ExtensionStore {
|
||||
client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
tmp_dir: dest_dir.to_string_lossy().to_string(),
|
||||
tmp_dir: dest_dir.to_proto(),
|
||||
extension: Some(missing_extension),
|
||||
})
|
||||
})?
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::{TypedEnvelope, proto};
|
||||
use client::{
|
||||
TypedEnvelope,
|
||||
proto::{self, FromProto},
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use extension::{
|
||||
Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionManifest,
|
||||
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
|
||||
ExtensionLanguageServerProxy, ExtensionManifest,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions, RenameOptions};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
|
||||
@@ -125,7 +128,7 @@ impl HeadlessExtensionStore {
|
||||
|
||||
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
|
||||
|
||||
debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
|
||||
debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load());
|
||||
|
||||
if manifest.version.as_ref() != extension.version.as_str() {
|
||||
anyhow::bail!(
|
||||
@@ -165,12 +168,13 @@ impl HeadlessExtensionStore {
|
||||
})?;
|
||||
}
|
||||
|
||||
if manifest.language_servers.is_empty() {
|
||||
if !manifest.allow_remote_load() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let wasm_extension: Arc<dyn Extension> =
|
||||
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
|
||||
let wasm_extension: Arc<dyn Extension> = Arc::new(
|
||||
WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?,
|
||||
);
|
||||
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
@@ -186,6 +190,28 @@ impl HeadlessExtensionStore {
|
||||
);
|
||||
})?;
|
||||
}
|
||||
log::info!("Loaded language server: {}", language_server_id);
|
||||
}
|
||||
|
||||
for (debug_adapter, meta) in &manifest.debug_adapters {
|
||||
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.proxy.register_debug_adapter(
|
||||
wasm_extension.clone(),
|
||||
debug_adapter.clone(),
|
||||
&extension_dir.join(schema_path),
|
||||
);
|
||||
})?;
|
||||
log::info!("Loaded debug adapter: {}", debug_adapter);
|
||||
}
|
||||
|
||||
for debug_locator in manifest.debug_locators.keys() {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.proxy
|
||||
.register_debug_locator(wasm_extension.clone(), debug_locator.clone());
|
||||
})?;
|
||||
log::info!("Loaded debug locator: {}", debug_locator);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -305,7 +331,7 @@ impl HeadlessExtensionStore {
|
||||
version: extension.version,
|
||||
dev: extension.dev,
|
||||
},
|
||||
PathBuf::from(envelope.payload.tmp_dir),
|
||||
PathBuf::from_proto(envelope.payload.tmp_dir),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
|
||||
@@ -999,7 +999,7 @@ impl Extension {
|
||||
) -> Result<Result<DebugRequest, String>> {
|
||||
match self {
|
||||
Extension::V0_6_0(ext) => {
|
||||
let build_config_template = resolved_build_task.into();
|
||||
let build_config_template = resolved_build_task.try_into()?;
|
||||
let dap_request = ext
|
||||
.call_run_dap_locator(store, &locator_name, &build_config_template)
|
||||
.await?
|
||||
|
||||
@@ -299,15 +299,17 @@ impl From<extension::DebugScenario> for DebugScenario {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpawnInTerminal> for ResolvedTask {
|
||||
fn from(value: SpawnInTerminal) -> Self {
|
||||
Self {
|
||||
impl TryFrom<SpawnInTerminal> for ResolvedTask {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: SpawnInTerminal) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
label: value.label,
|
||||
command: value.command,
|
||||
command: value.command.context("missing command")?,
|
||||
args: value.args,
|
||||
env: value.env.into_iter().collect(),
|
||||
cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,17 @@ impl FeatureFlag for JjUiFeatureFlag {
|
||||
const NAME: &'static str = "jj-ui";
|
||||
}
|
||||
|
||||
pub struct ZedCloudFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for ZedCloudFeatureFlag {
|
||||
const NAME: &'static str = "zed-cloud";
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
// Require individual opt-in, for now.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -15,16 +15,14 @@ use std::{
|
||||
};
|
||||
use ui::{Context, LabelLike, ListItem, Window};
|
||||
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
|
||||
use util::{maybe, paths::compare_paths};
|
||||
use util::{
|
||||
maybe,
|
||||
paths::{PathStyle, compare_paths},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OpenPathPrompt;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const PROMPT_ROOT: &str = "C:\\";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const PROMPT_ROOT: &str = "/";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenPathDelegate {
|
||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||
@@ -34,6 +32,8 @@ pub struct OpenPathDelegate {
|
||||
string_matches: Vec<StringMatch>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
prompt_root: String,
|
||||
path_style: PathStyle,
|
||||
replace_prompt: Task<()>,
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ impl OpenPathDelegate {
|
||||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||
lister: DirectoryLister,
|
||||
creating_path: bool,
|
||||
path_style: PathStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
tx: Some(tx),
|
||||
@@ -53,6 +54,11 @@ impl OpenPathDelegate {
|
||||
string_matches: Vec::new(),
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
prompt_root: match path_style {
|
||||
PathStyle::Posix => "/".to_string(),
|
||||
PathStyle::Windows => "C:\\".to_string(),
|
||||
},
|
||||
path_style,
|
||||
replace_prompt: Task::ready(()),
|
||||
}
|
||||
}
|
||||
@@ -185,7 +191,8 @@ impl OpenPathPrompt {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
||||
let delegate =
|
||||
OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
|
||||
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, window, cx);
|
||||
@@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let lister = &self.lister;
|
||||
let last_item = Path::new(&query)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
||||
(dir.to_string(), last_item.into_owned())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir == "" {
|
||||
dir = PROMPT_ROOT.to_string();
|
||||
}
|
||||
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
|
||||
|
||||
let query = match &self.directory_state {
|
||||
DirectoryState::List { parent_path, .. } => {
|
||||
@@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
|
||||
let parent_path_is_root = self.prompt_root == dir;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(query) = query {
|
||||
let paths = query.await;
|
||||
@@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
DirectoryState::None { create: false }
|
||||
| DirectoryState::List { .. } => match paths {
|
||||
Ok(paths) => DirectoryState::List {
|
||||
entries: path_candidates(&dir, paths),
|
||||
entries: path_candidates(parent_path_is_root, paths),
|
||||
parent_path: dir.clone(),
|
||||
error: None,
|
||||
},
|
||||
@@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
DirectoryState::None { create: true }
|
||||
| DirectoryState::Create { .. } => match paths {
|
||||
Ok(paths) => {
|
||||
let mut entries = path_candidates(&dir, paths);
|
||||
let mut entries = path_candidates(parent_path_is_root, paths);
|
||||
let mut exists = false;
|
||||
let mut is_dir = false;
|
||||
let mut new_id = None;
|
||||
@@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<String> {
|
||||
let candidate = self.get_entry(self.selected_index)?;
|
||||
let path_style = self.path_style;
|
||||
Some(
|
||||
maybe!({
|
||||
match &self.directory_state {
|
||||
@@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
parent_path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
path_style.separator()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
parent_path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
path_style.separator()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
DirectoryState::None { .. } => return,
|
||||
DirectoryState::List { parent_path, .. } => {
|
||||
let confirmed_path =
|
||||
if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
|
||||
PathBuf::from(PROMPT_ROOT)
|
||||
if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
|
||||
PathBuf::from(&self.prompt_root)
|
||||
} else {
|
||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||
.join(&candidate.path.string)
|
||||
@@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
return;
|
||||
}
|
||||
let prompted_path =
|
||||
if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
|
||||
PathBuf::from(PROMPT_ROOT)
|
||||
if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
|
||||
PathBuf::from(&self.prompt_root)
|
||||
} else {
|
||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||
.join(&user_input.file.string)
|
||||
@@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
if parent_path == PROMPT_ROOT {
|
||||
format!("{}{}", PROMPT_ROOT, candidate.path.string)
|
||||
if parent_path == &self.prompt_root {
|
||||
format!("{}{}", self.prompt_root, candidate.path.string)
|
||||
} else {
|
||||
candidate.path.string.clone()
|
||||
},
|
||||
@@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
user_input,
|
||||
..
|
||||
} => {
|
||||
let (label, delta) = if parent_path == PROMPT_ROOT {
|
||||
let (label, delta) = if parent_path == &self.prompt_root {
|
||||
(
|
||||
format!("{}{}", PROMPT_ROOT, candidate.path.string),
|
||||
PROMPT_ROOT.len(),
|
||||
format!("{}{}", self.prompt_root, candidate.path.string),
|
||||
self.prompt_root.len(),
|
||||
)
|
||||
} else {
|
||||
(candidate.path.string.clone(), 0)
|
||||
@@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
|
||||
if *parent_path == PROMPT_ROOT {
|
||||
fn path_candidates(
|
||||
parent_path_is_root: bool,
|
||||
mut children: Vec<DirectoryItem>,
|
||||
) -> Vec<CandidateInfo> {
|
||||
if parent_path_is_root {
|
||||
children.push(DirectoryItem {
|
||||
is_dir: true,
|
||||
path: PathBuf::default(),
|
||||
@@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Ve
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||
let last_item = Path::new(&query)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
||||
(dir.to_string(), last_item.into_owned())
|
||||
} else {
|
||||
(query.to_string(), String::new())
|
||||
};
|
||||
match path_style {
|
||||
PathStyle::Posix => {
|
||||
if dir.is_empty() {
|
||||
dir = "/".to_string();
|
||||
}
|
||||
}
|
||||
PathStyle::Windows => {
|
||||
if dir.len() < 3 {
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||
match path_style {
|
||||
PathStyle::Posix => {
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if !dir.ends_with('/') {
|
||||
dir.push('/');
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
PathStyle::Windows => {
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir.len() < 3 {
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
if !dir.ends_with('\\') {
|
||||
dir.push('\\');
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::paths::PathStyle;
|
||||
|
||||
use crate::open_path_prompt::get_dir_and_suffix;
|
||||
|
||||
#[test]
|
||||
fn test_get_dir_and_suffix_with_windows_style() {
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "Use");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||
assert_eq!(suffix, "Docum");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||
assert_eq!(suffix, "Documents");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
|
||||
assert_eq!(suffix, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_dir_and_suffix_with_posix_style() {
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "Use");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Docum");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Documents");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/Documents/");
|
||||
assert_eq!(suffix, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use ui::rems;
|
||||
use util::path;
|
||||
use util::{path, paths::PathStyle};
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use crate::OpenPathDelegate;
|
||||
@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
@@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root");
|
||||
@@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
@@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
// Support both forward and backward slashes.
|
||||
let query = "C:/root/";
|
||||
@@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||
async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
|
||||
|
||||
let query = "/root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
|
||||
|
||||
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
|
||||
let query = "/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
|
||||
|
||||
let query = "/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
@@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, true, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
|
||||
|
||||
insert_query(path!("/root"), &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
||||
@@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
fn build_open_path_prompt(
|
||||
project: Entity<Project>,
|
||||
creating_path: bool,
|
||||
path_style: PathStyle,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
||||
let (tx, _) = futures::channel::oneshot::channel();
|
||||
let lister = project::DirectoryLister::Project(project.clone());
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
(
|
||||
|
||||
@@ -2844,7 +2844,7 @@ impl GitPanel {
|
||||
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
|
||||
IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
)
|
||||
@@ -2965,15 +2965,20 @@ impl GitPanel {
|
||||
&self,
|
||||
id: impl Into<ElementId>,
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
ui::ButtonLike::new_rounded_right("commit-split-button-right")
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::None)
|
||||
.size(ButtonSize::None)
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.px_1()
|
||||
.h_full()
|
||||
.justify_center()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
|
||||
),
|
||||
)
|
||||
@@ -3066,6 +3071,7 @@ impl GitPanel {
|
||||
Some(
|
||||
self.panel_header_container(window, cx)
|
||||
.px_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
panel_button(change_string)
|
||||
.color(Color::Muted)
|
||||
@@ -3080,23 +3086,25 @@ impl GitPanel {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(div().flex_grow()) // spacer
|
||||
.child(self.render_overflow_menu("overflow_menu"))
|
||||
.child(div().w_2()) // another spacer
|
||||
.child(
|
||||
panel_filled_button(text)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
tooltip,
|
||||
action.as_ref(),
|
||||
&self.focus_handle,
|
||||
))
|
||||
.disabled(self.entry_count == 0)
|
||||
.on_click(move |_, _, cx| {
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}),
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_overflow_menu("overflow_menu"))
|
||||
.child(
|
||||
panel_filled_button(text)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
tooltip,
|
||||
action.as_ref(),
|
||||
&self.focus_handle,
|
||||
))
|
||||
.disabled(self.entry_count == 0)
|
||||
.on_click(move |_, _, cx| {
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -3174,7 +3182,7 @@ impl GitPanel {
|
||||
.w_full()
|
||||
.h(max_height + footer_size)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.cursor_text()
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
window.focus(&this.commit_editor.focus_handle(cx));
|
||||
@@ -3259,6 +3267,7 @@ impl GitPanel {
|
||||
let (can_commit, tooltip) = self.configure_commit_button(cx);
|
||||
let title = self.commit_button_title();
|
||||
let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.id("commit-wrapper")
|
||||
.on_hover(cx.listener(move |this, hovered, _, cx| {
|
||||
@@ -3371,6 +3380,7 @@ impl GitPanel {
|
||||
self.render_git_commit_menu(
|
||||
ElementId::Name(format!("split-button-right-{}", title).into()),
|
||||
Some(commit_tooltip_focus_handle.clone()),
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
))
|
||||
@@ -3415,8 +3425,8 @@ impl GitPanel {
|
||||
|
||||
fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.py_2()
|
||||
.px(px(8.))
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Label::new(
|
||||
@@ -3431,22 +3441,21 @@ impl GitPanel {
|
||||
let branch = active_repository.read(cx).branch.as_ref()?;
|
||||
let commit = branch.most_recent_commit.as_ref()?.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let this = cx.entity();
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.py_2()
|
||||
.px(px(8.))
|
||||
.border_color(cx.theme().colors().border)
|
||||
.py_1p5()
|
||||
.px_2()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.overflow_hidden()
|
||||
.items_center()
|
||||
.max_w(relative(0.85))
|
||||
.h_full()
|
||||
.child(
|
||||
Label::new(commit.subject.clone())
|
||||
.size(LabelSize::Small)
|
||||
@@ -3480,12 +3489,11 @@ impl GitPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.when(commit.has_parent, |this| {
|
||||
let has_unstaged = self.has_unstaged_changes();
|
||||
this.child(
|
||||
panel_icon_button("undo", IconName::Undo)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
@@ -3507,43 +3515,38 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h_full()
|
||||
.flex_grow()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(h_flex().w_full().justify_around().child(
|
||||
if self.active_repository.is_some() {
|
||||
"No changes to commit"
|
||||
} else {
|
||||
"No Git repositories"
|
||||
},
|
||||
))
|
||||
.children({
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
h_flex().h_full().flex_grow().justify_center().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(h_flex().w_full().justify_around().child(
|
||||
if self.active_repository.is_some() {
|
||||
"No changes to commit"
|
||||
} else {
|
||||
"No Git repositories"
|
||||
},
|
||||
))
|
||||
.children({
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
.text_ui_sm(cx)
|
||||
.mx_auto()
|
||||
.text_color(Color::Placeholder.color(cx)),
|
||||
)
|
||||
})
|
||||
.text_ui_sm(cx)
|
||||
.mx_auto()
|
||||
.text_color(Color::Placeholder.color(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(
|
||||
@@ -4621,7 +4624,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
repo_selector_trigger.disabled(single_repo).truncate(true),
|
||||
Tooltip::text("Switch active repository"),
|
||||
Tooltip::text("Switch Active Repository"),
|
||||
)
|
||||
.anchor(Corner::BottomLeft)
|
||||
.into_any_element();
|
||||
|
||||
@@ -487,7 +487,7 @@ impl Element for TextElement {
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let line = window
|
||||
.text_system()
|
||||
.shape_line(display_text, font_size, &runs);
|
||||
.shape_line(display_text, font_size, &runs, None);
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
let (selection, cursor) = if selected_range.is_empty() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
|
||||
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
|
||||
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
|
||||
point, size,
|
||||
ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
|
||||
Window, point, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -88,6 +88,8 @@ pub enum ScrollStrategy {
|
||||
/// May not be possible if there's not enough list items above the item scrolled to:
|
||||
/// in this case, the element will be placed at the closest possible position.
|
||||
Center,
|
||||
/// Scrolls the element to be at the given item index from the top of the viewport.
|
||||
ToPosition(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -140,6 +142,15 @@ impl UniformListScrollHandle {
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
|
||||
}
|
||||
|
||||
/// Checks if the list can be scrolled vertically.
|
||||
pub fn is_scrollable(&self) -> bool {
|
||||
if let Some(size) = self.0.borrow().last_item_size {
|
||||
size.contents.height > size.item.height
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for UniformList {
|
||||
@@ -345,6 +356,15 @@ impl Element for UniformList {
|
||||
}
|
||||
}
|
||||
}
|
||||
ScrollStrategy::ToPosition(sticky_index) => {
|
||||
let target_y_in_viewport = item_height * sticky_index;
|
||||
let target_scroll_top = item_top - target_y_in_viewport;
|
||||
let max_scroll_top =
|
||||
(content_height - list_height).max(Pixels::ZERO);
|
||||
let new_scroll_top =
|
||||
target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
|
||||
updated_scroll_offset.y = -new_scroll_top;
|
||||
}
|
||||
}
|
||||
scroll_offset = *updated_scroll_offset
|
||||
}
|
||||
@@ -354,6 +374,7 @@ impl Element for UniformList {
|
||||
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
|
||||
/ item_height)
|
||||
.ceil() as usize;
|
||||
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
@@ -409,6 +430,7 @@ impl Element for UniformList {
|
||||
let mut decoration = decoration.as_ref().compute(
|
||||
visible_range.clone(),
|
||||
bounds,
|
||||
scroll_offset,
|
||||
item_height,
|
||||
self.item_count,
|
||||
window,
|
||||
@@ -476,6 +498,7 @@ pub trait UniformListDecoration {
|
||||
&self,
|
||||
visible_range: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
scroll_offset: Point<Pixels>,
|
||||
item_height: Pixels,
|
||||
item_count: usize,
|
||||
window: &mut Window,
|
||||
@@ -483,6 +506,35 @@ pub trait UniformListDecoration {
|
||||
) -> AnyElement;
|
||||
}
|
||||
|
||||
/// A trait for implementing top slots in a [`UniformList`].
|
||||
/// Top slots are elements that appear at the top of the list and can adjust
|
||||
/// the visible range of list items.
|
||||
pub trait UniformListTopSlot {
|
||||
/// Returns elements to render at the top slot for the given visible range.
|
||||
fn compute(
|
||||
&mut self,
|
||||
visible_range: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> SmallVec<[AnyElement; 8]>;
|
||||
|
||||
/// Layout and prepaint the top slot elements.
|
||||
fn prepaint(
|
||||
&self,
|
||||
elements: &mut SmallVec<[AnyElement; 8]>,
|
||||
bounds: Bounds<Pixels>,
|
||||
item_height: Pixels,
|
||||
scroll_offset: Point<Pixels>,
|
||||
padding: crate::Edges<Pixels>,
|
||||
can_scroll_horizontally: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Paint the top slot elements.
|
||||
fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App);
|
||||
}
|
||||
|
||||
impl UniformList {
|
||||
/// Selects a specific list item for measurement.
|
||||
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||
|
||||
@@ -55,7 +55,7 @@ impl Keystroke {
|
||||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
|
||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
|
||||
@@ -200,8 +200,8 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
app_path = app_path.display()
|
||||
);
|
||||
|
||||
// execute the script using /bin/bash
|
||||
let restart_process = Command::new("/bin/bash")
|
||||
let restart_process = Command::new("/usr/bin/env")
|
||||
.arg("bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.process_group(0)
|
||||
|
||||
@@ -466,12 +466,7 @@ fn handle_keyup_msg(
|
||||
}
|
||||
|
||||
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let Some(input) = char::from_u32(wparam.0 as u32)
|
||||
.filter(|c| !c.is_control())
|
||||
.map(String::from)
|
||||
else {
|
||||
return Some(1);
|
||||
};
|
||||
let input = parse_char_message(wparam, &state_ptr)?;
|
||||
with_input_handler(&state_ptr, |input_handler| {
|
||||
input_handler.replace_text_in_range(None, &input);
|
||||
});
|
||||
@@ -1228,6 +1223,36 @@ fn handle_input_language_changed(
|
||||
Some(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
|
||||
let code_point = wparam.loword();
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
// https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630
|
||||
match code_point {
|
||||
0xD800..=0xDBFF => {
|
||||
// High surrogate, wait for low surrogate
|
||||
lock.pending_surrogate = Some(code_point);
|
||||
None
|
||||
}
|
||||
0xDC00..=0xDFFF => {
|
||||
if let Some(high_surrogate) = lock.pending_surrogate.take() {
|
||||
// Low surrogate, combine with pending high surrogate
|
||||
String::from_utf16(&[high_surrogate, code_point]).ok()
|
||||
} else {
|
||||
// Invalid low surrogate without a preceding high surrogate
|
||||
log::warn!(
|
||||
"Received low surrogate without a preceding high surrogate: {code_point:x}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
lock.pending_surrogate = None;
|
||||
String::from_utf16(&[code_point]).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
|
||||
let msg = MSG {
|
||||
@@ -1270,6 +1295,10 @@ where
|
||||
capslock: current_capslock(),
|
||||
}))
|
||||
}
|
||||
VK_PACKET => {
|
||||
translate_message(handle, wparam, lparam);
|
||||
None
|
||||
}
|
||||
VK_CAPITAL => {
|
||||
let capslock = current_capslock();
|
||||
if state
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct WindowsWindowState {
|
||||
|
||||
pub callbacks: Callbacks,
|
||||
pub input_handler: Option<PlatformInputHandler>,
|
||||
pub pending_surrogate: Option<u16>,
|
||||
pub last_reported_modifiers: Option<Modifiers>,
|
||||
pub last_reported_capslock: Option<Capslock>,
|
||||
pub system_key_handled: bool,
|
||||
@@ -105,6 +106,7 @@ impl WindowsWindowState {
|
||||
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
let pending_surrogate = None;
|
||||
let last_reported_modifiers = None;
|
||||
let last_reported_capslock = None;
|
||||
let system_key_handled = false;
|
||||
@@ -126,6 +128,7 @@ impl WindowsWindowState {
|
||||
min_size,
|
||||
callbacks,
|
||||
input_handler,
|
||||
pending_surrogate,
|
||||
last_reported_modifiers,
|
||||
last_reported_capslock,
|
||||
system_key_handled,
|
||||
|
||||
@@ -357,6 +357,7 @@ impl WindowTextSystem {
|
||||
text: SharedString,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
force_width: Option<Pixels>,
|
||||
) -> ShapedLine {
|
||||
debug_assert!(
|
||||
text.find('\n').is_none(),
|
||||
@@ -384,7 +385,7 @@ impl WindowTextSystem {
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self.layout_line(&text, font_size, runs);
|
||||
let layout = self.layout_line(&text, font_size, runs, force_width);
|
||||
|
||||
ShapedLine {
|
||||
layout,
|
||||
@@ -524,6 +525,7 @@ impl WindowTextSystem {
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
force_width: Option<Pixels>,
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
@@ -544,9 +546,9 @@ impl WindowTextSystem {
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self
|
||||
.line_layout_cache
|
||||
.layout_line(text, font_size, &font_runs);
|
||||
let layout =
|
||||
self.line_layout_cache
|
||||
.layout_line_internal(text, font_size, &font_runs, force_width);
|
||||
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
@@ -482,6 +482,7 @@ impl LineLayoutCache {
|
||||
font_size,
|
||||
runs,
|
||||
wrap_width,
|
||||
force_width: None,
|
||||
} as &dyn AsCacheKeyRef;
|
||||
|
||||
let current_frame = self.current_frame.upgradable_read();
|
||||
@@ -516,6 +517,7 @@ impl LineLayoutCache {
|
||||
font_size,
|
||||
runs: SmallVec::from(runs),
|
||||
wrap_width,
|
||||
force_width: None,
|
||||
});
|
||||
|
||||
let mut current_frame = self.current_frame.write();
|
||||
@@ -534,6 +536,20 @@ impl LineLayoutCache {
|
||||
font_size: Pixels,
|
||||
runs: &[FontRun],
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
SharedString: From<Text>,
|
||||
{
|
||||
self.layout_line_internal(text, font_size, runs, None)
|
||||
}
|
||||
|
||||
pub fn layout_line_internal<Text>(
|
||||
&self,
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[FontRun],
|
||||
force_width: Option<Pixels>,
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
SharedString: From<Text>,
|
||||
@@ -543,6 +559,7 @@ impl LineLayoutCache {
|
||||
font_size,
|
||||
runs,
|
||||
wrap_width: None,
|
||||
force_width,
|
||||
} as &dyn AsCacheKeyRef;
|
||||
|
||||
let current_frame = self.current_frame.upgradable_read();
|
||||
@@ -557,16 +574,30 @@ impl LineLayoutCache {
|
||||
layout
|
||||
} else {
|
||||
let text = SharedString::from(text);
|
||||
let layout = Arc::new(
|
||||
self.platform_text_system
|
||||
.layout_line(&text, font_size, runs),
|
||||
);
|
||||
let mut layout = self
|
||||
.platform_text_system
|
||||
.layout_line(&text, font_size, runs);
|
||||
|
||||
if let Some(force_width) = force_width {
|
||||
let mut glyph_pos = 0;
|
||||
for run in layout.runs.iter_mut() {
|
||||
for glyph in run.glyphs.iter_mut() {
|
||||
if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
|
||||
glyph.position.x = glyph_pos * force_width;
|
||||
}
|
||||
glyph_pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = Arc::new(CacheKey {
|
||||
text,
|
||||
font_size,
|
||||
runs: SmallVec::from(runs),
|
||||
wrap_width: None,
|
||||
force_width,
|
||||
});
|
||||
let layout = Arc::new(layout);
|
||||
current_frame.lines.insert(key.clone(), layout.clone());
|
||||
current_frame.used_lines.push(key);
|
||||
layout
|
||||
@@ -591,6 +622,7 @@ struct CacheKey {
|
||||
font_size: Pixels,
|
||||
runs: SmallVec<[FontRun; 1]>,
|
||||
wrap_width: Option<Pixels>,
|
||||
force_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -599,6 +631,7 @@ struct CacheKeyRef<'a> {
|
||||
font_size: Pixels,
|
||||
runs: &'a [FontRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
force_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl PartialEq for (dyn AsCacheKeyRef + '_) {
|
||||
@@ -622,6 +655,7 @@ impl AsCacheKeyRef for CacheKey {
|
||||
font_size: self.font_size,
|
||||
runs: self.runs.as_slice(),
|
||||
wrap_width: self.wrap_width,
|
||||
force_width: self.force_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +226,21 @@ impl HttpClientWithUrl {
|
||||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
pub fn build_zed_llm_url(
|
||||
&self,
|
||||
path: &str,
|
||||
query: &[(&str, &str)],
|
||||
use_cloud: bool,
|
||||
) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://llm.zed.dev",
|
||||
"https://zed.dev" => {
|
||||
if use_cloud {
|
||||
"https://cloud.zed.dev"
|
||||
} else {
|
||||
"https://llm.zed.dev"
|
||||
}
|
||||
}
|
||||
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
|
||||
@@ -835,10 +835,6 @@ impl InlineCompletionButton {
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.popover_menu_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for InlineCompletionButton {
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::time::Duration;
|
||||
use std::{fmt, io};
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
@@ -462,6 +462,10 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
|
||||
fn max_token_count(&self) -> u64;
|
||||
/// Returns the maximum token count for this model in burn mode (If `supports_burn_mode` is `false` this returns `None`)
|
||||
fn max_token_count_in_burn_mode(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
fn max_output_tokens(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
@@ -557,6 +561,18 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelExt: LanguageModel {
|
||||
fn max_token_count_for_mode(&self, mode: CompletionMode) -> u64 {
|
||||
match mode {
|
||||
CompletionMode::Normal => self.max_token_count(),
|
||||
CompletionMode::Max => self
|
||||
.max_token_count_in_burn_mode()
|
||||
.unwrap_or_else(|| self.max_token_count()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl LanguageModelExt for dyn LanguageModel {}
|
||||
|
||||
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
|
||||
fn name() -> String;
|
||||
fn description() -> String;
|
||||
|
||||
@@ -28,6 +28,7 @@ credentials_provider.workspace = true
|
||||
copilot.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
|
||||
@@ -2,6 +2,7 @@ use anthropic::AnthropicModelMode;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
};
|
||||
@@ -136,6 +137,7 @@ impl State {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
||||
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
|
||||
|
||||
Self {
|
||||
client: client.clone(),
|
||||
@@ -163,7 +165,7 @@ impl State {
|
||||
.await;
|
||||
}
|
||||
|
||||
let response = Self::fetch_models(client, llm_api_token).await?;
|
||||
let response = Self::fetch_models(client, llm_api_token, use_cloud).await?;
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let mut models = Vec::new();
|
||||
@@ -265,13 +267,18 @@ impl State {
|
||||
async fn fetch_models(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
use_cloud: bool,
|
||||
) -> Result<ListModelsResponse> {
|
||||
let http_client = &client.http_client();
|
||||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
||||
let request = http_client::Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(http_client.build_zed_llm_url("/models", &[])?.as_ref())
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/models", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.body(AsyncBody::empty())?;
|
||||
let mut response = http_client
|
||||
@@ -535,6 +542,7 @@ impl CloudLanguageModel {
|
||||
llm_api_token: LlmApiToken,
|
||||
app_version: Option<SemanticVersion>,
|
||||
body: CompletionBody,
|
||||
use_cloud: bool,
|
||||
) -> Result<PerformLlmCompletionResponse> {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
@@ -542,9 +550,11 @@ impl CloudLanguageModel {
|
||||
let mut refreshed_token = false;
|
||||
|
||||
loop {
|
||||
let request_builder = http_client::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref());
|
||||
let request_builder = http_client::Request::builder().method(Method::POST).uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/completions", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
);
|
||||
let request_builder = if let Some(app_version) = app_version {
|
||||
request_builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
|
||||
} else {
|
||||
@@ -730,6 +740,13 @@ impl LanguageModel for CloudLanguageModel {
|
||||
self.model.max_token_count as u64
|
||||
}
|
||||
|
||||
fn max_token_count_in_burn_mode(&self) -> Option<u64> {
|
||||
self.model
|
||||
.max_token_count_in_max_mode
|
||||
.filter(|_| self.model.supports_max_mode)
|
||||
.map(|max_token_count| max_token_count as u64)
|
||||
}
|
||||
|
||||
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
|
||||
match &self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
@@ -764,6 +781,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let model_id = self.model.id.to_string();
|
||||
let generate_content_request =
|
||||
into_google(request, model_id.clone(), GoogleModelMode::Default);
|
||||
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
|
||||
async move {
|
||||
let http_client = &client.http_client();
|
||||
let token = llm_api_token.acquire(&client).await?;
|
||||
@@ -779,7 +797,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/count_tokens", &[])?
|
||||
.build_zed_llm_url("/count_tokens", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -828,6 +846,9 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let intent = request.intent;
|
||||
let mode = request.mode;
|
||||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
let use_cloud = cx
|
||||
.update(|cx| cx.has_flag::<ZedCloudFeatureFlag>())
|
||||
.unwrap_or(false);
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
let request = into_anthropic(
|
||||
@@ -865,6 +886,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err.downcast::<ApiError>() {
|
||||
@@ -917,6 +939,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -957,6 +980,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use settings::SettingsStore;
|
||||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
use super::google::count_google_tokens;
|
||||
@@ -268,6 +269,19 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let is_user_initiated = request.intent.is_none_or(|intent| match intent {
|
||||
CompletionIntent::UserPrompt
|
||||
| CompletionIntent::ThreadContextSummarization
|
||||
| CompletionIntent::InlineAssist
|
||||
| CompletionIntent::TerminalInlineAssist
|
||||
| CompletionIntent::GenerateGitCommitMessage => true,
|
||||
|
||||
CompletionIntent::ToolResults
|
||||
| CompletionIntent::ThreadSummarization
|
||||
| CompletionIntent::CreateFile
|
||||
| CompletionIntent::EditFile => false,
|
||||
});
|
||||
|
||||
let copilot_request = match into_copilot_chat(&self.model, request) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
|
||||
@@ -276,7 +290,8 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
|
||||
let request_limiter = self.request_limiter.clone();
|
||||
let future = cx.spawn(async move |cx| {
|
||||
let request = CopilotChat::stream_completion(copilot_request, cx.clone());
|
||||
let request =
|
||||
CopilotChat::stream_completion(copilot_request, is_user_initiated, cx.clone());
|
||||
request_limiter
|
||||
.stream(async move {
|
||||
let response = request.await?;
|
||||
|
||||
@@ -24,7 +24,6 @@ gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
receiver: (parameter_list
|
||||
"(" @context
|
||||
(parameter_declaration
|
||||
name: (_) @name
|
||||
name: (_) @context
|
||||
type: (_) @context)
|
||||
")" @context)
|
||||
name: (field_identifier) @name
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
(comment) @comment.inclusive
|
||||
|
||||
[
|
||||
(string)
|
||||
(template_string)
|
||||
] @string
|
||||
(string) @string
|
||||
|
||||
(template_string (string_fragment) @string)
|
||||
|
||||
(jsx_element) @element
|
||||
|
||||
|
||||
@@ -34,5 +34,4 @@ decrease_indent_patterns = [
|
||||
{ pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] },
|
||||
{ pattern = "^\\s*except\\b.*:", valid_after = ["try", "except"] },
|
||||
{ pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] },
|
||||
{ pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] }
|
||||
]
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
(else_clause) @start.else
|
||||
(except_clause) @start.except
|
||||
(finally_clause) @start.finally
|
||||
(case_pattern) @start.case
|
||||
(case_clause) @start.case
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
(comment) @comment.inclusive
|
||||
|
||||
[
|
||||
(string)
|
||||
(template_string)
|
||||
] @string
|
||||
(string) @string
|
||||
|
||||
(template_string (string_fragment) @string)
|
||||
|
||||
(jsx_element) @element
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
(comment) @comment.inclusive
|
||||
|
||||
(string) @string
|
||||
|
||||
(template_string (string_fragment) @string)
|
||||
|
||||
(_ value: (call_expression
|
||||
function: (identifier) @function_name_before_type_arguments
|
||||
type_arguments: (type_arguments)))
|
||||
|
||||
25
crates/net/Cargo.toml
Normal file
25
crates/net/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "net"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/net.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
smol.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
anyhow.workspace = true
|
||||
async-io = "2.4"
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
1
crates/net/LICENSE-GPL
Symbolic link
1
crates/net/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
69
crates/net/src/async_net.rs
Normal file
69
crates/net/src/async_net.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use smol::net::unix::{UnixListener, UnixStream};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::{UnixListener, UnixStream};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows {
|
||||
use std::{
|
||||
io::Result,
|
||||
path::Path,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use smol::{
|
||||
Async,
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
};
|
||||
|
||||
pub struct UnixListener(Async<crate::UnixListener>);
|
||||
|
||||
impl UnixListener {
|
||||
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?))
|
||||
}
|
||||
|
||||
pub async fn accept(&self) -> Result<(UnixStream, ())> {
|
||||
let (sock, _) = self.0.read_with(|listener| listener.accept()).await?;
|
||||
Ok((UnixStream(Async::new(sock)?), ()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnixStream(Async<crate::UnixStream>);
|
||||
|
||||
impl UnixStream {
|
||||
pub async fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for UnixStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.0).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for UnixStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.0).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.0).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.0).poll_close(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
crates/net/src/listener.rs
Normal file
45
crates/net/src/listener.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{
|
||||
io::Result,
|
||||
os::windows::io::{AsSocket, BorrowedSocket},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen};
|
||||
|
||||
use crate::{
|
||||
socket::UnixSocket,
|
||||
stream::UnixStream,
|
||||
util::{init, map_ret, sockaddr_un},
|
||||
};
|
||||
|
||||
pub struct UnixListener(UnixSocket);
|
||||
|
||||
impl UnixListener {
|
||||
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
init();
|
||||
let socket = UnixSocket::new()?;
|
||||
let (addr, len) = sockaddr_un(path)?;
|
||||
unsafe {
|
||||
map_ret(bind(
|
||||
socket.as_raw(),
|
||||
&addr as *const _ as *const _,
|
||||
len as i32,
|
||||
))?;
|
||||
map_ret(listen(socket.as_raw(), SOMAXCONN as _))?;
|
||||
}
|
||||
Ok(Self(socket))
|
||||
}
|
||||
|
||||
pub fn accept(&self) -> Result<(UnixStream, ())> {
|
||||
let mut storage = SOCKADDR_UN::default();
|
||||
let mut len = std::mem::size_of_val(&storage) as i32;
|
||||
let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?;
|
||||
Ok((UnixStream::new(raw), ()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsSocket for UnixListener {
|
||||
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
|
||||
}
|
||||
}
|
||||
107
crates/net/src/net.rs
Normal file
107
crates/net/src/net.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
pub mod async_net;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod listener;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod socket;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod stream;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod util;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use listener::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use socket::*;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use std::os::unix::net::{UnixListener, UnixStream};
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use stream::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
const SERVER_MESSAGE: &str = "Connection closed";
|
||||
const CLIENT_MESSAGE: &str = "Hello, server!";
|
||||
const BUFFER_SIZE: usize = 32;
|
||||
|
||||
#[test]
|
||||
fn test_windows_listener() -> std::io::Result<()> {
|
||||
use crate::{UnixListener, UnixStream};
|
||||
|
||||
let temp = tempfile::tempdir()?;
|
||||
let socket = temp.path().join("socket.sock");
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
// Server
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
|
||||
// Read data from the client
|
||||
let mut buffer = [0; BUFFER_SIZE];
|
||||
let bytes_read = stream.read(&mut buffer).unwrap();
|
||||
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
assert_eq!(string, CLIENT_MESSAGE);
|
||||
|
||||
// Send a message back to the client
|
||||
stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap();
|
||||
});
|
||||
|
||||
// Client
|
||||
let mut client = UnixStream::connect(&socket)?;
|
||||
|
||||
// Send data to the server
|
||||
client.write_all(CLIENT_MESSAGE.as_bytes())?;
|
||||
let mut buffer = [0; BUFFER_SIZE];
|
||||
|
||||
// Read the response from the server
|
||||
let bytes_read = client.read(&mut buffer)?;
|
||||
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
assert_eq!(string, SERVER_MESSAGE);
|
||||
client.flush()?;
|
||||
|
||||
server.join().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unix_listener() -> std::io::Result<()> {
|
||||
use crate::async_net::{UnixListener, UnixStream};
|
||||
|
||||
smol::block_on(async {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let socket = temp.path().join("socket.sock");
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
// Server
|
||||
let server = smol::spawn(async move {
|
||||
let (mut stream, _) = listener.accept().await.unwrap();
|
||||
|
||||
// Read data from the client
|
||||
let mut buffer = [0; BUFFER_SIZE];
|
||||
let bytes_read = stream.read(&mut buffer).await.unwrap();
|
||||
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
assert_eq!(string, CLIENT_MESSAGE);
|
||||
|
||||
// Send a message back to the client
|
||||
stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap();
|
||||
});
|
||||
|
||||
// Client
|
||||
let mut client = UnixStream::connect(&socket).await?;
|
||||
client.write_all(CLIENT_MESSAGE.as_bytes()).await?;
|
||||
|
||||
// Read the response from the server
|
||||
let mut buffer = [0; BUFFER_SIZE];
|
||||
let bytes_read = client.read(&mut buffer).await?;
|
||||
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
assert_eq!(string, "Connection closed");
|
||||
client.flush().await?;
|
||||
|
||||
server.await;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
59
crates/net/src/socket.rs
Normal file
59
crates/net/src/socket.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation},
|
||||
Networking::WinSock::{
|
||||
AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED,
|
||||
WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::util::map_ret;
|
||||
|
||||
pub struct UnixSocket(SOCKET);
|
||||
|
||||
impl UnixSocket {
|
||||
pub fn new() -> Result<Self> {
|
||||
unsafe {
|
||||
let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?;
|
||||
SetHandleInformation(
|
||||
HANDLE(raw.0 as _),
|
||||
HANDLE_FLAG_INHERIT.0,
|
||||
HANDLE_FLAGS::default(),
|
||||
)?;
|
||||
Ok(Self(raw))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_raw(&self) -> SOCKET {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result<Self> {
|
||||
match unsafe { accept(self.0, Some(storage), Some(len)) } {
|
||||
Ok(sock) => Ok(Self(sock)),
|
||||
Err(err) => {
|
||||
let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 };
|
||||
if wsa_err == WSAEWOULDBLOCK.0 {
|
||||
Err(Error::new(ErrorKind::WouldBlock, "accept would block"))
|
||||
} else {
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recv(&self, buf: &mut [u8]) -> Result<usize> {
|
||||
map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) })
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, buf: &[u8]) -> Result<usize> {
|
||||
map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UnixSocket {
|
||||
fn drop(&mut self) {
|
||||
unsafe { closesocket(self.0) };
|
||||
}
|
||||
}
|
||||
60
crates/net/src/stream.rs
Normal file
60
crates/net/src/stream.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::{
|
||||
io::{Read, Result, Write},
|
||||
os::windows::io::{AsSocket, BorrowedSocket},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use async_io::IoSafe;
|
||||
use windows::Win32::Networking::WinSock::connect;
|
||||
|
||||
use crate::{
|
||||
socket::UnixSocket,
|
||||
util::{init, map_ret, sockaddr_un},
|
||||
};
|
||||
|
||||
pub struct UnixStream(UnixSocket);
|
||||
|
||||
unsafe impl IoSafe for UnixStream {}
|
||||
|
||||
impl UnixStream {
|
||||
pub fn new(socket: UnixSocket) -> Self {
|
||||
Self(socket)
|
||||
}
|
||||
|
||||
pub fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
init();
|
||||
unsafe {
|
||||
let inner = UnixSocket::new()?;
|
||||
let (addr, len) = sockaddr_un(path)?;
|
||||
|
||||
map_ret(connect(
|
||||
inner.as_raw(),
|
||||
&addr as *const _ as *const _,
|
||||
len as i32,
|
||||
))?;
|
||||
Ok(Self(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for UnixStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
self.0.recv(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for UnixStream {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
self.0.send(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsSocket for UnixStream {
|
||||
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
|
||||
}
|
||||
}
|
||||
76
crates/net/src/util.rs
Normal file
76
crates/net/src/util.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::{
|
||||
io::{Error, ErrorKind, Result},
|
||||
path::Path,
|
||||
sync::Once,
|
||||
};
|
||||
|
||||
use windows::Win32::Networking::WinSock::{
|
||||
ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup,
|
||||
};
|
||||
|
||||
pub(crate) fn init() {
|
||||
static ONCE: Once = Once::new();
|
||||
|
||||
ONCE.call_once(|| unsafe {
|
||||
let mut wsa_data = std::mem::zeroed();
|
||||
let result = WSAStartup(0x202, &mut wsa_data);
|
||||
if result != 0 {
|
||||
panic!("WSAStartup failed: {}", result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
|
||||
pub(crate) fn sockaddr_un<P: AsRef<Path>>(path: P) -> Result<(SOCKADDR_UN, usize)> {
|
||||
let mut addr = SOCKADDR_UN::default();
|
||||
addr.sun_family = ADDRESS_FAMILY(AF_UNIX);
|
||||
|
||||
let bytes = path
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.map(|s| s.as_bytes())
|
||||
.ok_or(ErrorKind::InvalidInput)?;
|
||||
|
||||
if bytes.contains(&0) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"paths may not contain interior null bytes",
|
||||
));
|
||||
}
|
||||
if bytes.len() >= addr.sun_path.len() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"path must be shorter than SUN_LEN",
|
||||
));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
bytes.as_ptr(),
|
||||
addr.sun_path.as_mut_ptr().cast(),
|
||||
bytes.len(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut len = sun_path_offset(&addr) + bytes.len();
|
||||
match bytes.first() {
|
||||
Some(&0) | None => {}
|
||||
Some(_) => len += 1,
|
||||
}
|
||||
Ok((addr, len))
|
||||
}
|
||||
|
||||
pub(crate) fn map_ret(ret: i32) -> Result<usize> {
|
||||
if ret == SOCKET_ERROR {
|
||||
Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 }))
|
||||
} else {
|
||||
Ok(ret as usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn sun_path_offset(addr: &SOCKADDR_UN) -> usize {
|
||||
// Work with an actual instance of the type since using a null pointer is UB
|
||||
let base = addr as *const _ as usize;
|
||||
let path = &addr.sun_path as *const _ as usize;
|
||||
path - base
|
||||
}
|
||||
@@ -67,10 +67,10 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
|
||||
|
||||
pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
|
||||
let id = ElementId::Name(id.into());
|
||||
ui::IconButton::new(id, icon)
|
||||
|
||||
IconButton::new(id, icon)
|
||||
// TODO: Change this once we use on_surface_bg in button_like
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::Compact)
|
||||
}
|
||||
|
||||
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user