Compare commits

..

22 Commits

Author SHA1 Message Date
Junkui Zhang
2eac6a9222 fix linux ci 2025-06-11 18:16:59 +08:00
Junkui Zhang
7c3cffdc52 clippy 2025-06-11 17:32:18 +08:00
Junkui Zhang
5a3186b659 fix neovim 2025-06-11 16:37:37 +08:00
Junkui Zhang
caf54844de remove international keycodes 2025-06-11 16:33:11 +08:00
Junkui Zhang
745ebe2313 fix all tests 2025-06-11 16:16:31 +08:00
Junkui Zhang
5c95e942e6 fix 2025-06-10 23:35:53 +08:00
Junkui Zhang
f979f24bfa try fix linux 2025-06-10 23:32:00 +08:00
Junkui Zhang
411b9abb9e clippy 2025-06-10 23:13:43 +08:00
Junkui Zhang
81d4d48ef2 add missing codes 2025-06-10 23:11:21 +08:00
Junkui Zhang
cd9284761a fix macOS 2025-06-10 23:01:45 +08:00
Junkui Zhang
34f9eef879 try fix macOS 2025-06-10 22:14:46 +08:00
Junkui Zhang
23cf6bf268 init macOS 2025-06-10 21:16:23 +08:00
Junkui Zhang
c97e477eb1 fix test platform 2025-06-10 20:58:39 +08:00
Junkui Zhang
16804a81cc checkpoint 2025-06-10 18:27:35 +08:00
Junkui Zhang
8bf39bf768 fix 2025-06-10 18:16:21 +08:00
Junkui Zhang
75922e8fcd fix 2025-06-10 17:58:05 +08:00
Junkui Zhang
2eb83364ae support parsing scan code 2025-06-10 17:58:05 +08:00
Junkui Zhang
5d22585ef5 add shifted key tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
71303fa18b add tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
5753b978a0 impl for windows 2025-06-10 17:58:04 +08:00
Junkui Zhang
9cf2490ed7 add PlatformKeyboardMapper 2025-06-10 17:58:04 +08:00
Junkui Zhang
28ea3ea529 add ScanCode 2025-06-10 17:58:04 +08:00
314 changed files with 9983 additions and 12077 deletions

View File

@@ -22,7 +22,7 @@ runs:
- name: Check for broken links
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' './docs/src/**/*'
args: --no-progress './docs/src/**/*'
fail: true
- name: Build book

View File

@@ -1,6 +1,12 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -20,4 +26,9 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi

View File

@@ -319,6 +319,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |
@@ -498,7 +500,6 @@ jobs:
needs:
- job_spec
- style
- check_docs
- migration_checks
# run_tests: If adding required tests, add them here and to script below.
- workspace_hack
@@ -516,8 +517,7 @@ jobs:
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
@@ -803,7 +803,6 @@ jobs:
name: Build with Nix
uses: ./.github/workflows/nix.yml
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
secrets: inherit
with:
flake-output: debug
# excludes the final package to only cache dependencies

View File

@@ -30,7 +30,6 @@ jobs:
noop:
name: No-op
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- name: No-op
run: echo "Nothing to do"

View File

@@ -214,7 +214,6 @@ jobs:
bundle-nix:
name: Build and cache Nix package
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
update-nightly-tag:

View File

@@ -19,7 +19,6 @@ env:
jobs:
unit_evals:
if: github.repository_owner == 'zed-industries'
timeout-minutes: 60
name: Run unit evals
runs-on:

229
Cargo.lock generated
View File

@@ -491,6 +491,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"gpui",
"shlex",
"smol",
"tempfile",
"util",
@@ -2822,11 +2823,9 @@ dependencies = [
"collections",
"credentials_provider",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"hickory-resolver",
"http_client",
"http_client_tls",
"httparse",
@@ -2835,7 +2834,6 @@ dependencies = [
"paths",
"postage",
"rand 0.8.5",
"regex",
"release_channel",
"rpc",
"rustls-pki-types",
@@ -3542,20 +3540,6 @@ dependencies = [
"coreaudio-sys",
]
[[package]]
name = "coreaudio-rs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
dependencies = [
"bitflags 1.3.2",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "coreaudio-sys"
version = "0.2.16"
@@ -3591,8 +3575,7 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50"
dependencies = [
"alsa",
"core-foundation-sys",
@@ -3602,7 +3585,7 @@ dependencies = [
"js-sys",
"libc",
"mach2",
"ndk 0.8.0",
"ndk",
"ndk-context",
"oboe",
"wasm-bindgen",
@@ -3611,32 +3594,6 @@ dependencies = [
"windows 0.54.0",
]
[[package]]
name = "cpal"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
dependencies = [
"alsa",
"coreaudio-rs 0.13.0",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk 0.9.0",
"ndk-context",
"num-derive",
"num-traits",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.54.0",
]
[[package]]
name = "cpp_demangle"
version = "0.4.4"
@@ -4070,7 +4027,6 @@ dependencies = [
"gpui",
"http_client",
"language",
"libc",
"log",
"node_runtime",
"parking_lot",
@@ -4094,7 +4050,7 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
dependencies = [
"schemars",
"serde",
@@ -4242,7 +4198,6 @@ dependencies = [
"gpui",
"serde_json",
"task",
"util",
"workspace-hack",
]
@@ -4740,6 +4695,7 @@ dependencies = [
"client",
"clock",
"collections",
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
@@ -4908,18 +4864,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "enumflags2"
version = "0.7.11"
@@ -6177,7 +6121,6 @@ dependencies = [
"anyhow",
"askpass",
"buffer_diff",
"call",
"chrono",
"collections",
"command_palette_hooks",
@@ -7498,51 +7441,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hickory-proto"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.8.5",
"thiserror 1.0.69",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror 1.0.69",
"tokio",
"tracing",
]
[[package]]
name = "hidden-trait"
version = "0.1.2"
@@ -8412,18 +8310,6 @@ dependencies = [
"windows 0.58.0",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg 0.50.0",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -9117,6 +9003,7 @@ dependencies = [
"tree-sitter-yaml",
"unindent",
"util",
"which 6.0.3",
"workspace",
"workspace-hack",
]
@@ -9308,12 +9195,6 @@ dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -9440,7 +9321,7 @@ dependencies = [
"core-foundation 0.10.0",
"core-video",
"coreaudio-rs 0.12.1",
"cpal 0.16.0",
"cpal",
"futures 0.3.31",
"gpui",
"gpui_tokio",
@@ -9574,15 +9455,6 @@ dependencies = [
"hashbrown 0.15.3",
]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "lsp"
version = "0.1.0"
@@ -10219,21 +10091,7 @@ dependencies = [
"bitflags 2.9.0",
"jni-sys",
"log",
"ndk-sys 0.5.0+25.2.9519653",
"num_enum",
"thiserror 1.0.69",
]
[[package]]
name = "ndk"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.9.0",
"jni-sys",
"log",
"ndk-sys 0.6.0+11769913",
"ndk-sys",
"num_enum",
"thiserror 1.0.69",
]
@@ -10253,15 +10111,6 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "ndk-sys"
version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
"jni-sys",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -10675,43 +10524,6 @@ dependencies = [
"objc2-quartz-core",
]
[[package]]
name = "objc2-audio-toolbox"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
dependencies = [
"bitflags 2.9.0",
"libc",
"objc2",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-core-audio"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
dependencies = [
"dispatch2",
"objc2",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "objc2-core-audio-types"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1"
dependencies = [
"bitflags 2.9.0",
"objc2",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.1"
@@ -10817,7 +10629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni",
"ndk 0.8.0",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
@@ -13414,7 +13226,6 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.4.9",
"hickory-resolver",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -13471,12 +13282,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "resolv-conf"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "resvg"
version = "0.45.1"
@@ -13596,7 +13401,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
dependencies = [
"cpal 0.15.3",
"cpal",
"hound",
]
@@ -14608,12 +14413,12 @@ dependencies = [
"fs",
"gpui",
"log",
"paths",
"schemars",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]
@@ -15944,7 +15749,6 @@ dependencies = [
"theme",
"thiserror 2.0.12",
"url",
"urlencoding",
"util",
"windows 0.61.1",
"workspace-hack",
@@ -17352,14 +17156,12 @@ dependencies = [
"itertools 0.14.0",
"libc",
"log",
"nix 0.29.0",
"rand 0.8.5",
"regex",
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
"shlex",
"smol",
"take-until",
"tempfile",
@@ -18401,12 +18203,6 @@ dependencies = [
"wasite",
]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "wiggle"
version = "29.0.1"
@@ -19527,7 +19323,6 @@ dependencies = [
"num-rational",
"num-traits",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"object",
@@ -19948,7 +19743,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.192.0"
version = "0.191.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -433,10 +433,9 @@ convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -524,7 +523,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"rustls-tls-native-roots",
"socks",
"stream",
"hickory-dns",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
@@ -684,7 +682,9 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
@@ -698,8 +698,6 @@ codegen-units = 16
[profile.dev.package]
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 }
cranelift-codegen-shared = { opt-level = 3 }
resvg = { opt-level = 3 }
rustybuzz = { opt-level = 3 }
ttf-parser = { opt-level = 3 }

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -115,7 +115,6 @@
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "git::Blame",
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",

View File

@@ -139,7 +139,6 @@
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "git::Blame",
"cmd-alt-g m": "git::OpenModifiedFiles",
"cmd-i": "editor::ShowSignatureHelp",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",

View File

@@ -184,8 +184,6 @@
"z f": "editor::FoldSelectedRanges",
"z shift-m": "editor::FoldAll",
"z shift-r": "editor::UnfoldAll",
"z l": "vim::ColumnRight",
"z h": "vim::ColumnLeft",
"shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
"shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
// Count support
@@ -397,8 +395,6 @@
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
".": "vim::Repeat",
"alt-.": "vim::RepeatFind",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
@@ -425,7 +421,6 @@
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
@@ -455,8 +450,7 @@
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow",
"alt-shift-c": "editor::AddSelectionAbove"
"shift-c": "editor::AddSelectionBelow"
}
},
{

View File

@@ -27,11 +27,11 @@ If you are unsure how to fulfill the user's request, gather more information wit
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{abs_path}}`
- `{{root_name}}`
{{/each}}
- Bias towards not asking the user for help if you can find the answer yourself.
- When providing paths to tools, the path should always start with the name of a project root directory listed above.
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
{{# if (has_tool 'grep') }}
- When looking for symbols in the project, prefer the `grep` tool.

View File

@@ -307,8 +307,6 @@
// "all"
// 4. Draw whitespaces at boundaries only:
// "boundary"
// 5. Draw whitespaces only after non-whitespace characters:
// "trailing"
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
@@ -447,9 +445,7 @@
// Whether to show breakpoints in the gutter.
"breakpoints": true,
// Whether to show fold buttons in the gutter.
"folds": true,
// Minimum number of characters to reserve space for in the gutter.
"min_line_number_digits": 4
"folds": true
},
"indent_guides": {
// Whether to show indent guides in the editor.
@@ -1482,8 +1478,7 @@
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
},
"debuggers": ["Delve"]
}
},
"GraphQL": {
"prettier": {
@@ -1548,15 +1543,9 @@
"Plain Text": {
"allow_rewrap": "anywhere"
},
"Python": {
"debuggers": ["Debugpy"]
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
},
"Rust": {
"debuggers": ["CodeLLDB"]
},
"SCSS": {
"prettier": {
"allowed": true

View File

@@ -1,4 +1,4 @@
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
// Static tasks configuration.
//
// Example:
[

View File

@@ -7,10 +7,7 @@ use gpui::{
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
LanguageServerStatusUpdate, ServerHealth,
};
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent,
@@ -19,7 +16,6 @@ use project::{
use smallvec::SmallVec;
use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
@@ -34,9 +30,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
actions!(activity_indicator, [ShowErrorMessage]);
pub enum Event {
ShowStatus {
server_name: LanguageServerName,
status: SharedString,
ShowError {
server_name: SharedString,
error: String,
},
}
@@ -49,8 +45,8 @@ pub struct ActivityIndicator {
#[derive(Debug)]
struct ServerStatus {
name: LanguageServerName,
status: LanguageServerStatusUpdate,
name: SharedString,
status: BinaryStatus,
}
struct PendingWork<'a> {
@@ -149,19 +145,19 @@ impl ActivityIndicator {
});
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
Event::ShowStatus {
server_name,
status,
} => {
Event::ShowError { server_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let status = status.clone();
let error = error.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create_buffer.await?;
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(0..0, format!("Language server {server_name}:\n\n{status}"))],
[(
0..0,
format!("Language server error: {}\n\n{}", server_name, error),
)],
None,
cx,
);
@@ -170,10 +166,7 @@ impl ActivityIndicator {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
editor.set_read_only(true);
editor
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
})),
None,
true,
@@ -192,34 +185,19 @@ impl ActivityIndicator {
}
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
let mut status_message_shown = false;
self.statuses.retain(|status| match &status.status {
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
if !status_message_shown =>
{
cx.emit(Event::ShowStatus {
self.statuses.retain(|status| {
if let BinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
server_name: status.name.clone(),
status: SharedString::from(error),
error: error.clone(),
});
status_message_shown = true;
false
} else {
true
}
LanguageServerStatusUpdate::Health(
ServerHealth::Error | ServerHealth::Warning,
status_string,
) if !status_message_shown => match status_string {
Some(error) => {
cx.emit(Event::ShowStatus {
server_name: status.name.clone(),
status: error.clone(),
});
status_message_shown = true;
false
}
None => false,
},
_ => true,
});
cx.notify();
}
fn dismiss_error_message(
@@ -289,52 +267,48 @@ impl ActivityIndicator {
});
}
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
progress_token,
progress,
..
}) = pending_work.next()
{
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
let mut message = progress
.title
.as_deref()
.unwrap_or(progress_token)
.to_string();
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
),
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
});
if let Some(percentage) = progress.percentage {
write!(&mut message, " ({}%)", percentage).unwrap();
}
if let Some(progress_message) = progress.message.as_ref() {
message.push_str(": ");
message.push_str(progress_message);
}
let additional_work_count = pending_work.count();
if additional_work_count > 0 {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message,
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
tooltip_message: None,
});
}
if let Some(session) = self
@@ -395,38 +369,14 @@ impl ActivityIndicator {
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
let mut health_messages = SmallVec::<[_; 3]>::new();
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
for status in &self.statuses {
match &status.status {
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
checking_for_update.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
downloading.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
failed.push(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
Some(server_status) => {
health_messages.push((status.name.clone(), *health, server_status.clone()));
}
None => {
servers_to_clear_statuses.insert(status.name.clone());
}
},
match status.status {
BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
BinaryStatus::Downloading => downloading.push(status.name.clone()),
BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
BinaryStatus::None => {}
}
}
self.statuses
.retain(|status| !servers_to_clear_statuses.contains(&status.name));
health_messages.sort_by_key(|(_, health, _)| match health {
ServerHealth::Error => 2,
ServerHealth::Warning => 1,
ServerHealth::Ok => 0,
});
if !downloading.is_empty() {
return Some(Content {
@@ -507,7 +457,7 @@ impl ActivityIndicator {
}),
),
on_click: Some(Arc::new(|this, window, cx| {
this.show_error_message(&ShowErrorMessage, window, cx)
this.show_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
});
@@ -521,7 +471,7 @@ impl ActivityIndicator {
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Formatting failed: {failure}. Click to see logs."),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|indicator, window, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
@@ -532,56 +482,6 @@ impl ActivityIndicator {
});
}
// Show any health messages for the language servers
if let Some((server_name, health, message)) = health_messages.pop() {
let health_str = match health {
ServerHealth::Ok => format!("({server_name}) "),
ServerHealth::Warning => format!("({server_name}) Warning: "),
ServerHealth::Error => format!("({server_name}) Error: "),
};
let single_line_message = message
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() { None } else { Some(line) }
})
.collect::<Vec<_>>()
.join(" ");
let mut altered_message = single_line_message != message;
let truncated_message = truncate_and_trailoff(
&single_line_message,
MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
);
altered_message |= truncated_message != single_line_message;
let final_message = format!("{health_str}{truncated_message}");
let tooltip_message = if altered_message {
Some(format!("{health_str}{message}"))
} else {
None
};
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
message: final_message,
tooltip_message,
on_click: Some(Arc::new(move |activity_indicator, window, cx| {
if altered_message {
activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
} else {
activity_indicator
.statuses
.retain(|status| status.name != server_name);
cx.notify();
}
})),
});
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {

View File

@@ -1605,7 +1605,6 @@ impl ActiveThread {
this.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.cancel_last_completion(Some(window.window_handle()), cx);
thread.send_to_model(
model.model,
CompletionIntent::UserPrompt,
@@ -1681,10 +1680,7 @@ impl ActiveThread {
let editor = cx.new(|cx| {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: 4,
},
editor::EditorMode::AutoHeight { max_lines: 4 },
buffer,
None,
window,
@@ -3710,7 +3706,7 @@ mod tests {
use util::path;
use workspace::CollaboratorId;
use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
use crate::{ContextLoadResult, thread_store};
use super::*;
@@ -3844,114 +3840,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (cx, active_thread, _, thread, model) =
setup_test_environment(cx, project.clone()).await;
cx.update(|_, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: Arc::new(FakeLanguageModelProvider),
model: model.clone(),
}),
cx,
);
});
});
// Track thread events to verify cancellation
let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let _subscription = cx.update(|_, cx| {
let cancellation_events = cancellation_events.clone();
let new_request_events = new_request_events.clone();
cx.subscribe(
&thread,
move |_thread, event: &ThreadEvent, _cx| match event {
ThreadEvent::CompletionCanceled => {
cancellation_events.lock().unwrap().push(());
}
ThreadEvent::NewRequest => {
new_request_events.lock().unwrap().push(());
}
_ => {}
},
)
});
// Insert a user message and start streaming a response
let message = thread.update(cx, |thread, cx| {
let message_id = thread.insert_user_message(
"Hello, how are you?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread.advance_prompt_id();
thread.send_to_model(
model.clone(),
CompletionIntent::UserPrompt,
cx.active_window(),
cx,
);
thread.message(message_id).cloned().unwrap()
});
cx.run_until_parked();
// Verify that a completion is in progress
assert!(cx.read(|cx| thread.read(cx).is_generating()));
assert_eq!(new_request_events.lock().unwrap().len(), 1);
// Edit the message while the completion is still running
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(
message.id,
message.segments.as_slice(),
message.creases.as_slice(),
window,
cx,
);
let editor = active_thread
.editing_message
.as_ref()
.unwrap()
.1
.editor
.clone();
editor.update(cx, |editor, cx| {
editor.set_text("What is the weather like?", window, cx);
});
active_thread.confirm_editing_message(&Default::default(), window, cx);
});
cx.run_until_parked();
// Verify that the previous completion was cancelled
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
// Verify that a new request was started after cancellation
assert_eq!(new_request_events.lock().unwrap().len(), 2);
// Verify that the edited message contains the new text
let edited_message =
thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
match &edited_message.segments[0] {
MessageSegment::Text(text) => {
assert_eq!(text, "What is the weather like?");
}
_ => panic!("Expected text segment"),
}
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -162,7 +162,7 @@ pub fn init(
assistant_slash_command::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry, fs.clone(), cx);
context_server_configuration::init(language_registry, cx);
register_slash_commands(cx);
inline_assistant::init(

View File

@@ -586,7 +586,7 @@ impl AgentConfiguration {
if let Some(server) =
this.get_server(&context_server_id)
{
this.start_server(server, cx);
this.start_server(server, cx).log_err();
}
})
}

View File

@@ -1,6 +1,7 @@
use context_server::ContextServerCommand;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use project::project_settings::{ContextServerSettings, ProjectSettings};
use project::project_settings::{ContextServerConfiguration, ProjectSettings};
use serde_json::json;
use settings::update_settings_file;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
@@ -80,12 +81,13 @@ impl AddContextServerModal {
update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
settings.context_servers.insert(
name.into(),
ContextServerSettings::Custom {
command: ContextServerCommand {
ContextServerConfiguration {
command: Some(ContextServerCommand {
path,
args,
env: None,
},
}),
settings: Some(json!({})),
},
);
});

View File

@@ -15,7 +15,7 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
context_server_store::{ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
project_settings::{ContextServerConfiguration, ProjectSettings},
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
@@ -89,7 +89,7 @@ impl ConfigureContextServerModal {
}),
settings_validator,
settings_editor: cx.new(|cx| {
let mut editor = Editor::auto_height(1, 16, window, cx);
let mut editor = Editor::auto_height(16, window, cx);
editor.set_text(config.default_settings.trim(), window, cx);
editor.set_show_gutter(false, cx);
editor.set_soft_wrap_mode(
@@ -175,9 +175,8 @@ impl ConfigureContextServerModal {
let settings_changed = ProjectSettings::get_global(cx)
.context_servers
.get(&id.0)
.map_or(true, |settings| match settings {
ContextServerSettings::Custom { .. } => false,
ContextServerSettings::Extension { settings } => settings != &settings_value,
.map_or(true, |config| {
config.settings.as_ref() != Some(&settings_value)
});
let is_running = self.context_server_store.read(cx).status_for_server(&id)
@@ -222,12 +221,17 @@ impl ConfigureContextServerModal {
update_settings_file::<ProjectSettings>(workspace.read(cx).app_state().fs.clone(), cx, {
let id = id.clone();
|settings, _| {
settings.context_servers.insert(
id.0,
ContextServerSettings::Extension {
settings: settings_value,
},
);
if let Some(server_config) = settings.context_servers.get_mut(&id.0) {
server_config.settings = Some(settings_value);
} else {
settings.context_servers.insert(
id.0,
ContextServerConfiguration {
settings: Some(settings_value),
..Default::default()
},
);
}
}
});
}

View File

@@ -31,7 +31,7 @@ use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
item::{BreadcrumbText, ItemEvent, TabContentParams},
searchable::SearchableItemHandle,
};
use zed_actions::assistant::ToggleFocus;
@@ -532,12 +532,12 @@ impl Item for AgentDiffPane {
fn save(
&mut self,
options: SaveOptions,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(options, project, window, cx)
self.editor.save(format, project, window, cx)
}
fn save_as(
@@ -1513,7 +1513,7 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
let new_state = if thread.read(cx).is_generating() {
let new_state = if thread.read(cx).has_pending_edit_tool_uses() {
EditorState::Generating
} else {
EditorState::Reviewing

View File

@@ -91,13 +91,12 @@ impl AgentModelSelector {
impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
Button::new("active-model", model_name)

View File

@@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary,
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
make_lsp_adapter_delegate, render_remaining_tokens,
};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
@@ -29,8 +29,7 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
@@ -2354,6 +2353,24 @@ impl AgentPanel {
self.thread.clone().into_any_element()
}
fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return Some(ConfigurationError::NoProvider);
};
if !model.provider.is_authenticated(cx) {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
if model.provider.must_accept_terms(cx) {
return Some(ConfigurationError::ProviderPendingTermsAcceptance(
model.provider,
));
}
None
}
fn render_thread_empty_state(
&self,
window: &mut Window,
@@ -2363,9 +2380,7 @@ impl AgentPanel {
.history_store
.update(cx, |this, cx| this.recent_entries(6, cx));
let model_registry = LanguageModelRegistry::read_global(cx);
let configuration_error =
model_registry.configuration_error(model_registry.default_model(), cx);
let configuration_error = self.configuration_error(cx);
let no_error = configuration_error.is_none();
let focus_handle = self.focus_handle(cx);
@@ -2382,7 +2397,11 @@ impl AgentPanel {
.justify_center()
.items_center()
.gap_1()
.child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
.child(
h_flex().child(
Headline::new("Welcome to the Agent Panel")
),
)
.when(no_error, |parent| {
parent
.child(
@@ -2406,10 +2425,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
NewThread::default().boxed_clone(),
cx,
)
window.dispatch_action(NewThread::default().boxed_clone(), cx)
}),
)
.child(
@@ -2426,10 +2442,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
ToggleContextPicker.boxed_clone(),
cx,
)
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
}),
)
.child(
@@ -2446,10 +2459,7 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
ToggleModelSelector.boxed_clone(),
cx,
)
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
.child(
@@ -2466,50 +2476,51 @@ impl AgentPanel {
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
})
.map(|parent| match configuration_error_ref {
Some(
err @ (ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider),
) => parent
.child(h_flex().child(
Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
))
.child(
Button::new("settings", "Configure a Provider")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
.map(|parent| {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
h_flex().child(
Label::new("To start using the agent, configure at least one LLM provider.")
.color(Color::Muted)
.mb_2p5()
)
}),
),
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx,
))
)
.child(
Button::new("settings", "Configure a Provider")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart,
cx,
),
)
}
None => parent,
}
None => parent,
}),
})
)
})
.when(!recent_history.is_empty(), |parent| {
@@ -2544,8 +2555,7 @@ impl AgentPanel {
&self.focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
).map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
@@ -2555,68 +2565,79 @@ impl AgentPanel {
.child(
v_flex()
.gap_1()
.children(recent_history.into_iter().enumerate().map(
|(index, entry)| {
.children(
recent_history.into_iter().enumerate().map(|(index, entry)| {
// TODO: Add keyboard navigation.
let is_hovered =
self.hovered_recent_history_item == Some(index);
let is_hovered = self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
.hovered(is_hovered)
.on_hover(cx.listener(
move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item
== Some(index)
{
this.hovered_recent_history_item = None;
}
cx.notify();
},
))
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item == Some(index) {
this.hovered_recent_history_item = None;
}
cx.notify();
}))
.into_any_element()
},
)),
}),
)
)
.map(|parent| match configuration_error_ref {
Some(
err @ (ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider),
) => parent.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(Label::new(err.to_string()).size(LabelSize::Small))
.action_slot(
Button::new("settings", "Configure Provider")
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
.map(|parent| {
match configuration_error_ref {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(
Label::new(
"Configure at least one LLM provider to start using the panel.",
)
.size(LabelSize::Small),
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}),
),
),
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(Banner::new().severity(ui::Severity::Warning).child(
h_flex().w_full().children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
)),
))
.action_slot(
Button::new("settings", "Configure Provider")
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
cx,
)
}),
),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent
.child(
Banner::new()
.severity(ui::Severity::Warning)
.child(
h_flex()
.w_full()
.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
cx,
),
),
),
)
}
None => parent,
}
None => parent,
})
})
}

View File

@@ -1066,7 +1066,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::path;
use util::{path, separator};
use workspace::{AppState, Item};
#[test]
@@ -1217,14 +1217,14 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
separator!("a/one.txt"),
separator!("a/two.txt"),
separator!("a/three.txt"),
separator!("a/four.txt"),
separator!("b/five.txt"),
separator!("b/six.txt"),
separator!("b/seven.txt"),
separator!("b/eight.txt"),
];
let mut opened_editors = Vec::new();

View File

@@ -3,21 +3,16 @@ use std::sync::Arc;
use anyhow::Context as _;
use context_server::ContextServerId;
use extension::{ContextServerConfiguration, ExtensionManifest};
use fs::Fs;
use gpui::Task;
use language::LanguageRegistry;
use project::{
context_server_store::registry::ContextServerDescriptorRegistry,
project_settings::ProjectSettings,
};
use settings::update_settings_file;
use project::context_server_store::registry::ContextServerDescriptorRegistry;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
use crate::agent_configuration::ConfigureContextServerModal;
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx: &mut App) {
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
cx.observe_new(move |_: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
@@ -26,7 +21,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
cx.subscribe_in(extension_events, window, {
let language_registry = language_registry.clone();
let fs = fs.clone();
move |workspace, _, event, window, cx| match event {
extension::Event::ExtensionInstalled(manifest) => {
show_configure_mcp_modal(
@@ -37,13 +31,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
cx,
);
}
extension::Event::ExtensionUninstalled(manifest) => {
remove_context_server_settings(
manifest.context_servers.keys().cloned().collect(),
fs.clone(),
cx,
);
}
extension::Event::ConfigureExtensionRequested(manifest) => {
if !manifest.context_servers.is_empty() {
show_configure_mcp_modal(
@@ -68,18 +55,6 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
.detach();
}
fn remove_context_server_settings(
context_server_ids: Vec<Arc<str>>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| {
settings
.context_servers
.retain(|server_id, _| !context_server_ids.contains(server_id));
});
}
pub enum Configuration {
NotAvailable(ContextServerId, Option<SharedString>),
Required(
@@ -96,10 +71,6 @@ fn show_configure_mcp_modal(
window: &mut Window,
cx: &mut Context<'_, Workspace>,
) {
if !window.is_window_active() {
return;
}
let context_server_store = workspace.project().read(cx).context_server_store();
let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());

View File

@@ -24,7 +24,6 @@ use gpui::{
WeakEntity, Window, point,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::ConfigurationError;
use language_model::ConfiguredModel;
use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow;
@@ -39,7 +38,8 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use util::RangeExt;
use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
@@ -233,9 +233,10 @@ impl InlineAssistant {
return;
};
let configuration_error = || {
let model_registry = LanguageModelRegistry::read_global(cx);
model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
let is_authenticated = || {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(false, |model| model.provider.is_authenticated(cx))
};
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
@@ -283,23 +284,20 @@ impl InlineAssistant {
}
};
if let Some(error) = configuration_error() {
if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
cx.spawn(async move |_, cx| {
cx.update(|cx| provider.authenticate(cx))?.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if configuration_error().is_none() {
handle_assist(window, cx);
}
} else {
cx.spawn_in(window, async move |_, cx| {
if is_authenticated() {
handle_assist(window, cx);
} else {
cx.spawn_in(window, async move |_workspace, cx| {
let Some(task) = cx.update(|_, cx| {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(None, |model| Some(model.provider.authenticate(cx)))
})?
else {
let answer = cx
.prompt(
gpui::PromptLevel::Warning,
&error.to_string(),
"No language model provider configured",
None,
&["Configure", "Cancel"],
)
@@ -313,12 +311,17 @@ impl InlineAssistant {
.ok();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
return Ok(());
};
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if is_authenticated() {
handle_assist(window, cx);
}
} else {
handle_assist(window, cx);
}
}
@@ -765,6 +768,9 @@ impl InlineAssistant {
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { .. } => {
// This only matters for the terminal inline assistant
}
@@ -1165,31 +1171,27 @@ impl InlineAssistant {
selections.select_anchor_ranges([position..position])
});
let mut scroll_target_range = None;
let mut scroll_target_top;
let mut scroll_target_bottom;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_range = maybe!({
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
Some((top, bottom))
});
if scroll_target_range.is_none() {
log::error!("bug: failed to find blocks for scrolling to inline assist");
}
}
let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
} else {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
let top = start_row.0 as f32;
let bottom = top + 1.0;
(top, bottom)
});
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;

View File

@@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> {
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, window, cx);
@@ -403,7 +403,9 @@ impl<T: 'static> PromptEditor<T> {
CodegenStatus::Idle => {
cx.emit(PromptEditorEvent::StartRequested);
}
CodegenStatus::Pending => {}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
@@ -829,6 +831,7 @@ pub enum PromptEditorEvent {
StopRequested,
ConfirmRequested { execute: bool },
CancelRequested,
DismissRequested,
Resized { height_in_lines: u8 },
}
@@ -869,7 +872,6 @@ impl PromptEditor<BufferCodegen> {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
min_lines: 1,
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,
@@ -1048,7 +1050,6 @@ impl PromptEditor<TerminalCodegen> {
let prompt_editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
min_lines: 1,
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,

View File

@@ -39,9 +39,7 @@ use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*,
};
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent;
@@ -81,7 +79,6 @@ pub struct MessageEditor {
_subscriptions: Vec<Subscription>,
}
const MIN_EDITOR_LINES: usize = 4;
const MAX_EDITOR_LINES: usize = 8;
pub(crate) fn create_editor(
@@ -105,7 +102,6 @@ pub(crate) fn create_editor(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: MAX_EDITOR_LINES,
},
buffer,
@@ -257,7 +253,6 @@ impl MessageEditor {
})
} else {
editor.set_mode(EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: MAX_EDITOR_LINES,
})
}
@@ -433,6 +428,10 @@ impl MessageEditor {
}
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.thread.read(cx).has_pending_edit_tool_uses() {
return;
}
self.edits_expanded = true;
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
cx.notify();
@@ -507,47 +506,7 @@ impl MessageEditor {
cx.notify();
}
fn handle_reject_file_changes(
&mut self,
buffer: Entity<Buffer>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).has_pending_edit_tool_uses() {
return;
}
self.thread.update(cx, |thread, cx| {
let buffer_snapshot = buffer.read(cx);
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
thread
.reject_edits_in_ranges(buffer, vec![start..end], cx)
.detach();
});
cx.notify();
}
fn handle_accept_file_changes(
&mut self,
buffer: Entity<Buffer>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).has_pending_edit_tool_uses() {
return;
}
self.thread.update(cx, |thread, cx| {
let buffer_snapshot = buffer.read(cx);
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
thread.keep_edits_in_range(buffer, start..end, cx);
});
cx.notify();
}
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
@@ -682,87 +641,96 @@ impl MessageEditor {
.border_color(cx.theme().colors().border)
.child(
h_flex()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.when(focus_handle.is_focused(window), |this| {
this.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
.child(
h_flex()
.gap_1()
.when(focus_handle.is_focused(window), |this| {
this.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
)
}),
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window
.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
)
}),
),
)
.child(
v_flex()
.size_full()
.gap_1()
.gap_4()
.when(is_editor_expanded, |this| {
this.h(vh(0.8, window)).justify_between()
})
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
.child(
v_flex()
.min_h_16()
.when(is_editor_expanded, |this| this.h_full())
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx));
let line_height = settings.buffer_line_height.value() * font_size;
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
};
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
.into_any()
})
EditorElement::new(
&self.editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
.into_any()
}),
)
.child(
h_flex()
.flex_none()
.flex_wrap()
.justify_between()
.child(
h_flex()
.child(self.render_follow_toggle(cx))
.children(self.render_burn_mode_toggle(cx)),
.children(self.render_max_mode_toggle(cx)),
)
.child(
h_flex()
.gap_1()
.flex_wrap()
.when(!incompatible_tools.is_empty(), |this| {
this.child(
IconButton::new(
@@ -1030,7 +998,7 @@ impl MessageEditor {
this.handle_review_click(window, cx)
})),
)
.child(Divider::vertical().color(DividerColor::Border))
.child(ui::Divider::vertical().color(ui::DividerColor::Border))
.child(
Button::new("reject-all-changes", "Reject All")
.label_size(LabelSize::Small)
@@ -1080,7 +1048,7 @@ impl MessageEditor {
let file = buffer.read(cx).file()?;
let path = file.path();
let file_path = path.parent().and_then(|parent| {
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
@@ -1099,7 +1067,7 @@ impl MessageEditor {
}
});
let file_name = path.file_name().map(|name| {
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
@@ -1114,22 +1082,36 @@ impl MessageEditor {
.size(IconSize::Small)
});
let hover_color = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let overlay_gradient_hover = linear_gradient(
90.,
linear_color_stop(hover_color, 1.),
linear_color_stop(hover_color.opacity(0.2), 0.),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.cursor_pointer()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(editor_bg_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(border_color).border_b_1()
})
@@ -1144,75 +1126,47 @@ impl MessageEditor {
.child(
h_flex()
.gap_0p5()
.children(file_name)
.children(file_path),
.children(name_label)
.children(parent_label),
), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
)
.child(
h_flex()
.gap_1()
.visible_on_hover("edited-code")
.child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(
buffer.clone(),
window,
cx,
);
})
}),
)
.child(
Divider::vertical().color(DividerColor::BorderVariant),
)
.child(
Button::new("reject-file", "Reject")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_reject_file_changes(
buffer.clone(),
window,
cx,
);
})
}),
)
.child(
Button::new("accept-file", "Accept")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_accept_file_changes(
buffer.clone(),
window,
cx,
);
})
}),
),
div().visible_on_hover("edited-code").child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(
buffer.clone(),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_full()
.h_5_6()
.w_12()
.top_0()
.bottom_0()
.right(px(152.))
.bg(overlay_gradient),
);
.right(px(52.))
.bg(overlay_gradient)
.group_hover("edited-code", |style| {
style.bg(overlay_gradient_hover)
}),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
});
Some(element)
},
@@ -1229,7 +1183,6 @@ impl MessageEditor {
.map_or(false, |model| {
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
});
if !is_using_zed_provider {
return None;
}
@@ -1284,6 +1237,14 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
let message = "Start a new thread from a summary to continue the conversation.";
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
Icon::new(IconName::X)
.color(Color::Error)
@@ -1294,43 +1255,19 @@ impl MessageEditor {
.size(IconSize::XSmall)
};
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
"Thread reaching the token limit soon"
};
Some(
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.line_height(line_height)
.icon(icon)
.title(title)
.description(
"To continue, start a new thread from a summary or turn burn mode on.",
)
.primary_action(
Button::new("start-new-thread", "Start New Thread")
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
window.dispatch_action(
Box::new(NewThread { from_thread_id }),
cx,
);
})),
)
.secondary_action(
IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_burn_mode(&ToggleBurnMode, window, cx);
})),
),
),
.child(ui::Callout::multi_line(
title,
message,
icon,
"Start New Thread",
Box::new(cx.listener(|this, _, window, cx| {
let from_thread_id = Some(this.thread.read(cx).id().clone());
window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
})),
))
.line_height(line_height),
)
}
@@ -1527,8 +1464,6 @@ impl Render for MessageEditor {
total_token_usage.ratio()
});
let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -1545,7 +1480,7 @@ impl Render for MessageEditor {
if usage_callout.is_some() {
usage_callout
} else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
} else if token_usage_ratio != TokenUsageRatio::Normal {
self.render_token_limit_callout(line_height, token_usage_ratio, cx)
} else {
None

View File

@@ -1,3 +1 @@
[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
These files changed since last read:

View File

@@ -167,6 +167,9 @@ impl TerminalInlineAssistant {
PromptEditorEvent::CancelRequested => {
self.finish_assist(assist_id, true, false, window, cx);
}
PromptEditorEvent::DismissRequested => {
self.dismiss_assist(assist_id, window, cx);
}
PromptEditorEvent::Resized { height_in_lines } => {
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
}

View File

@@ -1389,7 +1389,7 @@ impl Thread {
request.messages[message_ix_to_cache].cache = true;
}
self.attach_tracked_files_state(&mut request.messages, cx);
self.attached_tracked_files_state(&mut request.messages, cx);
request.tools = available_tools;
request.mode = if model.supports_max_mode() {
@@ -1453,57 +1453,43 @@ impl Thread {
request
}
fn attach_tracked_files_state(
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
cx: &App,
) {
let mut stale_files = String::new();
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
let mut stale_message = String::new();
let action_log = self.action_log.read(cx);
for stale_file in action_log.stale_buffers(cx) {
if let Some(file) = stale_file.read(cx).file() {
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
let Some(file) = stale_file.read(cx).file() else {
continue;
};
if stale_message.is_empty() {
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER.trim()).ok();
}
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
}
if stale_files.is_empty() {
return;
let mut content = Vec::with_capacity(2);
if !stale_message.is_empty() {
content.push(stale_message.into());
}
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
let content = MessageContent::Text(
format!("{STALE_FILES_HEADER}{stale_files}").replace("\r\n", "\n"),
);
if !content.is_empty() {
let context_message = LanguageModelRequestMessage {
role: Role::User,
content,
cache: false,
};
// Insert our message before the last Assistant message.
// Inserting it to the tail distracts the agent too much
let insert_position = messages
.iter()
.enumerate()
.rfind(|(_, message)| message.role == Role::Assistant)
.map_or(messages.len(), |(i, _)| i);
let request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![content],
cache: false,
};
messages.insert(insert_position, request_message);
// It makes no sense to cache messages after this one because
// the cache is invalidated when this message is gone.
// Move the cache marker before this message.
let has_cached_messages_after = messages
.iter()
.skip(insert_position + 1)
.any(|message| message.cache);
if has_cached_messages_after {
messages[insert_position - 1].cache = true;
messages.push(context_message);
}
}
@@ -3309,24 +3295,12 @@ fn main() {{
assert_eq!(last_message.role, Role::User);
// Check the exact content of the message
let expected_content = "[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
- code.rs
";
let expected_content = "These files changed since last read:\n- code.rs\n";
assert_eq!(
last_message.string_contents(),
expected_content,
"Last message should be exactly the stale buffer notification"
);
// The message before the notification should be cached
let index = new_request.messages.len() - 2;
let previous_message = new_request.messages.get(index).unwrap();
assert!(
previous_message.cache,
"Message before the stale buffer notification should be cached"
);
}
#[gpui::test]

View File

@@ -594,11 +594,10 @@ impl Render for ThreadHistory {
view.pr_5()
.child(
uniform_list(
cx.entity().clone(),
"thread-history",
self.list_item_count(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx)
}),
Self::list_items,
)
.p_1()
.track_scroll(self.scroll_handle.clone())

View File

@@ -305,19 +305,17 @@ impl ThreadStore {
project: Entity<Project>,
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
root_name,
abs_path,
rules_file: None,
};
let root_name = worktree.read(cx).root_name().into();
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
let Some(rules_task) = rules_task else {
return Task::ready((context, None));
return Task::ready((
WorktreeContext {
root_name,
rules_file: None,
},
None,
));
};
cx.spawn(async move |_| {
@@ -330,8 +328,11 @@ impl ThreadStore {
}),
),
};
context.rules_file = rules_file;
(context, rules_file_error)
let worktree_info = WorktreeContext {
root_name,
rules_file,
};
(worktree_info, rules_file_error)
})
}
@@ -340,12 +341,12 @@ impl ThreadStore {
project: Entity<Project>,
cx: &mut App,
) -> Option<Task<Result<RulesFileContext>>> {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
worktree_ref
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())

View File

@@ -2,7 +2,7 @@ use client::zed_urls;
use component::{empty_example, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use language_model::RequestUsage;
use ui::{Callout, prelude::*};
use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
use zed_llm_client::{Plan, UsageLimit};
#[derive(IntoElement, RegisterComponent)]
@@ -91,23 +91,16 @@ impl RenderOnce for UsageCallout {
.size(IconSize::XSmall)
};
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.title(title)
.description(message)
.primary_action(
Button::new("upgrade", button_text)
.label_size(LabelSize::Small)
.on_click(move |_, _, cx| {
cx.open_url(&url);
}),
),
)
.into_any_element()
Callout::multi_line(
title,
message,
icon,
button_text,
Box::new(move |_, _, cx| {
cx.open_url(&url);
}),
)
.into_any_element()
}
}
@@ -196,8 +189,10 @@ impl Component for UsageCallout {
);
Some(
v_flex()
div()
.p_4()
.flex()
.flex_col()
.gap_4()
.child(free_examples)
.child(trial_examples)

View File

@@ -33,6 +33,15 @@ pub enum AnthropicModelMode {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
@@ -48,15 +57,6 @@ pub enum Model {
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -90,66 +90,46 @@ impl Model {
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-thinking") {
return Ok(Self::ClaudeOpus4Thinking);
}
if id.starts_with("claude-opus-4") {
return Ok(Self::ClaudeOpus4);
}
if id.starts_with("claude-sonnet-4-thinking") {
return Ok(Self::ClaudeSonnet4Thinking);
}
if id.starts_with("claude-sonnet-4") {
return Ok(Self::ClaudeSonnet4);
}
if id.starts_with("claude-3-7-sonnet-thinking") {
return Ok(Self::Claude3_7SonnetThinking);
}
if id.starts_with("claude-3-7-sonnet") {
return Ok(Self::Claude3_7Sonnet);
}
if id.starts_with("claude-3-5-sonnet") {
return Ok(Self::Claude3_5Sonnet);
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
} else if id.starts_with("claude-3-sonnet") {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
anyhow::bail!("invalid model id {id}");
}
if id.starts_with("claude-3-5-haiku") {
return Ok(Self::Claude3_5Haiku);
}
if id.starts_with("claude-3-opus") {
return Ok(Self::Claude3Opus);
}
if id.starts_with("claude-3-sonnet") {
return Ok(Self::Claude3Sonnet);
}
if id.starts_with("claude-3-haiku") {
return Ok(Self::Claude3Haiku);
}
Err(anyhow!("invalid model ID: {id}"))
}
pub fn id(&self) -> &str {
match self {
Self::ClaudeOpus4 => "claude-opus-4-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
Self::Claude3Opus => "claude-3-opus-latest",
Self::Claude3Sonnet => "claude-3-sonnet-20240229",
Self::Claude3Haiku => "claude-3-haiku-20240307",
Model::ClaudeOpus4 => "claude-opus-4-latest",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
@@ -157,24 +137,24 @@ impl Model {
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
Self::Claude3Opus => "claude-3-opus-latest",
Self::Claude3Sonnet => "claude-3-sonnet-20240229",
Self::Claude3Haiku => "claude-3-haiku-20240307",
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
pub fn display_name(&self) -> &str {
match self {
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Model::ClaudeOpus4 => "Claude Opus 4",
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Model::ClaudeSonnet4 => "Claude Sonnet 4",
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -230,15 +210,15 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -267,17 +247,17 @@ impl Model {
pub fn mode(&self) -> AnthropicModelMode {
match self {
Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3_5Sonnet
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),
@@ -288,7 +268,7 @@ impl Model {
pub fn beta_headers(&self) -> String {
let mut headers = Self::DEFAULT_BETA_HEADERS
.iter()
.into_iter()
.map(|header| header.to_string())
.collect::<Vec<_>>();

View File

@@ -15,6 +15,7 @@ path = "src/askpass.rs"
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
shlex.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true

View File

@@ -16,8 +16,6 @@ use smol::fs;
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
#[cfg(unix)]
use util::ResultExt as _;
#[cfg(unix)]
use util::get_shell_safe_zed_path;
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -162,6 +160,38 @@ impl AskPassSession {
}
}
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to determine current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
// (see https://github.com/zed-industries/zed/issues/29819)
// The zed path failing to execute within the askpass script results in very vague ssh
// authentication failed errors, so this was done to try and surface a better error
//
// use std::os::unix::fs::MetadataExt;
// let metadata = std::fs::metadata(&zed_path)
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
// anyhow::ensure!(
// is_executable,
// "Failed to verify Zed executable path for use in askpass"
// );
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
// errors are introduced in the future :(
let zed_path_escaped = shlex::try_quote(&zed_path)
.context("Failed to shell-escape Zed executable path for use in askpass")?;
return Ok(zed_path_escaped.to_string());
}
/// 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)]

View File

@@ -39,7 +39,7 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use multi_buffer::MultiBufferRow;
@@ -1887,8 +1887,6 @@ impl ContextEditor {
// value to not show the nudge.
let nudge = Some(false);
let model_registry = LanguageModelRegistry::read_global(cx);
if nudge.map_or(false, |value| value) {
Some(
h_flex()
@@ -1937,9 +1935,14 @@ impl ContextEditor {
)
.into_any_element(),
)
} else if let Some(configuration_error) =
model_registry.configuration_error(model_registry.default_model(), cx)
{
} else if let Some(configuration_error) = configuration_error(cx) {
let label = match configuration_error {
ConfigurationError::NoProvider => "No LLM provider selected.",
ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
ConfigurationError::ProviderPendingTermsAcceptance(_) => {
"LLM provider requires accepting the Terms of Service."
}
};
Some(
h_flex()
.px_3()
@@ -1956,7 +1959,7 @@ impl ContextEditor {
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(configuration_error.to_string())),
.child(Label::new(label)),
)
.child(
Button::new("open-configuration", "Configure Providers")
@@ -2031,19 +2034,14 @@ impl ContextEditor {
/// Will return false if the selected provided has a configuration error or
/// if the user has not accepted the terms of service for this provider.
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(configuration_error) =
model_registry.configuration_error(model_registry.default_model(), cx)
else {
return false;
};
let model = LanguageModelRegistry::read_global(cx).default_model();
match configuration_error {
ConfigurationError::NoProvider
| ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) => true,
ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
}
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& model
.as_ref()
.map_or(false, |model| model.provider.must_accept_terms(cx));
has_configuration_error || needs_to_accept_terms
}
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -3182,6 +3180,33 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
}
}
pub enum ConfigurationError {
NoProvider,
ProviderNotAuthenticated,
ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
}
fn configuration_error(cx: &App) -> Option<ConfigurationError> {
let model = LanguageModelRegistry::read_global(cx).default_model();
let is_authenticated = model
.as_ref()
.map_or(false, |model| model.provider.is_authenticated(cx));
if model.is_some() && is_authenticated {
return None;
}
if model.is_none() {
return Some(ConfigurationError::NoProvider);
}
if !is_authenticated {
return Some(ConfigurationError::ProviderNotAuthenticated);
}
None
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),

View File

@@ -582,7 +582,7 @@ mod test {
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt;
use util::path;
use util::{path, separator};
use super::collect_files;
@@ -627,7 +627,7 @@ mod test {
.await
.unwrap();
assert!(result_1.text.starts_with(path!("root/dir")));
assert!(result_1.text.starts_with(separator!("root/dir")));
// 4 files + 2 directories
assert_eq!(result_1.sections.len(), 6);
@@ -643,7 +643,7 @@ mod test {
cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
assert!(result.text.starts_with(path!("root/dir")));
assert!(result.text.starts_with(separator!("root/dir")));
// 5 files + 2 directories
assert_eq!(result.sections.len(), 7);
@@ -691,20 +691,24 @@ mod test {
.unwrap();
// Sanity check
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
assert_eq!(result.sections.len(), 7);
// Ensure that full file paths are included in the real output
assert!(
result
.text
.contains(path!("zed/assets/themes/andromeda/LICENSE"))
.contains(separator!("zed/assets/themes/andromeda/LICENSE"))
);
assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
assert!(
result
.text
.contains(path!("zed/assets/themes/summercamp/LICENSE"))
.contains(separator!("zed/assets/themes/ayu/LICENSE"))
);
assert!(
result
.text
.contains(separator!("zed/assets/themes/summercamp/LICENSE"))
);
assert_eq!(result.sections[5].label, "summercamp");
@@ -712,17 +716,17 @@ mod test {
// Ensure that things are in descending order, with properly relativized paths
assert_eq!(
result.sections[0].label,
path!("zed/assets/themes/andromeda/LICENSE")
separator!("zed/assets/themes/andromeda/LICENSE")
);
assert_eq!(result.sections[1].label, "andromeda");
assert_eq!(
result.sections[2].label,
path!("zed/assets/themes/ayu/LICENSE")
separator!("zed/assets/themes/ayu/LICENSE")
);
assert_eq!(result.sections[3].label, "ayu");
assert_eq!(
result.sections[4].label,
path!("zed/assets/themes/summercamp/LICENSE")
separator!("zed/assets/themes/summercamp/LICENSE")
);
// Ensure that the project lasts until after the last await
@@ -763,28 +767,31 @@ mod test {
.await
.unwrap();
assert!(result.text.starts_with(path!("zed/assets/themes\n")));
assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
assert_eq!(
result.sections[0].label,
separator!("zed/assets/themes/LICENSE")
);
assert_eq!(
result.sections[1].label,
path!("zed/assets/themes/summercamp/LICENSE")
separator!("zed/assets/themes/summercamp/LICENSE")
);
assert_eq!(
result.sections[2].label,
path!("zed/assets/themes/summercamp/subdir/LICENSE")
separator!("zed/assets/themes/summercamp/subdir/LICENSE")
);
assert_eq!(
result.sections[3].label,
path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
);
assert_eq!(result.sections[4].label, "subsubdir");
assert_eq!(result.sections[5].label, "subdir");
assert_eq!(result.sections[6].label, "summercamp");
assert_eq!(result.sections[7].label, path!("zed/assets/themes"));
assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));
assert_eq!(
result.text,
path!(
separator!(
"zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
)
);

View File

@@ -456,18 +456,18 @@ impl ActionLog {
})?
}
/// Track a buffer as read by agent, so we can notify the model about user edits.
/// Track a buffer as read, so we can notify the model about user edits.
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer_internal(buffer, false, cx);
}
/// Mark a buffer as created by agent, so we can refresh it in the context
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.track_buffer_internal(buffer.clone(), true, cx);
}
/// Mark a buffer as edited by agent, so we can refresh it in the context
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;

View File

@@ -8,7 +8,6 @@ use crate::{Template, Templates};
use anyhow::Result;
use assistant_tool::ActionLog;
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
pub use edit_parser::EditFormat;
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
use futures::{
Stream, StreamExt,
@@ -42,23 +41,13 @@ impl Template for CreateFilePromptTemplate {
}
#[derive(Serialize)]
struct EditFileXmlPromptTemplate {
struct EditFilePromptTemplate {
path: Option<PathBuf>,
edit_description: String,
}
impl Template for EditFileXmlPromptTemplate {
const TEMPLATE_NAME: &'static str = "edit_file_prompt_xml.hbs";
}
#[derive(Serialize)]
struct EditFileDiffFencedPromptTemplate {
path: Option<PathBuf>,
edit_description: String,
}
impl Template for EditFileDiffFencedPromptTemplate {
const TEMPLATE_NAME: &'static str = "edit_file_prompt_diff_fenced.hbs";
impl Template for EditFilePromptTemplate {
const TEMPLATE_NAME: &'static str = "edit_file_prompt.hbs";
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -81,7 +70,6 @@ pub struct EditAgent {
action_log: Entity<ActionLog>,
project: Entity<Project>,
templates: Arc<Templates>,
edit_format: EditFormat,
}
impl EditAgent {
@@ -90,14 +78,12 @@ impl EditAgent {
project: Entity<Project>,
action_log: Entity<ActionLog>,
templates: Arc<Templates>,
edit_format: EditFormat,
) -> Self {
EditAgent {
model,
project,
action_log,
templates,
edit_format,
}
}
@@ -223,23 +209,14 @@ impl EditAgent {
let this = self.clone();
let (events_tx, events_rx) = mpsc::unbounded();
let conversation = conversation.clone();
let edit_format = self.edit_format;
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
let prompt = match edit_format {
EditFormat::XmlTags => EditFileXmlPromptTemplate {
path,
edit_description,
}
.render(&this.templates)?,
EditFormat::DiffFenced => EditFileDiffFencedPromptTemplate {
path,
edit_description,
}
.render(&this.templates)?,
};
let prompt = EditFilePromptTemplate {
path,
edit_description,
}
.render(&this.templates)?;
let edit_chunks = this
.request(conversation, CompletionIntent::EditFile, prompt, cx)
.await?;
@@ -259,7 +236,7 @@ impl EditAgent {
self.action_log
.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?;
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, self.edit_format, cx);
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
let mut edit_events = edit_events.peekable();
while let Some(edit_event) = Pin::new(&mut edit_events).peek().await {
// Skip events until we're at the start of a new edit.
@@ -309,13 +286,7 @@ impl EditAgent {
_ => {
let ranges = resolved_old_text
.into_iter()
.map(|text| {
let start_line =
(snapshot.offset_to_point(text.range.start).row + 1) as usize;
let end_line =
(snapshot.offset_to_point(text.range.end).row + 1) as usize;
start_line..end_line
})
.map(|text| text.range)
.collect();
output_events
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
@@ -373,7 +344,6 @@ impl EditAgent {
fn parse_edit_chunks(
chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
edit_format: EditFormat,
cx: &mut AsyncApp,
) -> (
Task<Result<EditAgentOutput>>,
@@ -383,7 +353,7 @@ impl EditAgent {
let output = cx.background_spawn(async move {
pin_mut!(chunks);
let mut parser = EditParser::new(edit_format);
let mut parser = EditParser::new();
let mut raw_edits = String::new();
while let Some(chunk) = chunks.next().await {
match chunk {
@@ -459,25 +429,25 @@ impl EditAgent {
let task = cx.background_spawn(async move {
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
while let Some(edit_event) = edit_events.next().await {
let EditParserEvent::OldTextChunk {
chunk,
done,
line_hint,
} = edit_event?
else {
let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else {
break;
};
old_range_tx.send(matcher.push(&chunk, line_hint))?;
old_range_tx.send(matcher.push(&chunk))?;
if done {
break;
}
}
let matches = matcher.finish();
let best_match = matcher.select_best_match();
old_range_tx.send(best_match.clone())?;
let old_range = if matches.len() == 1 {
matches.first()
} else {
// No matches or multiple ambiguous matches
None
};
old_range_tx.send(old_range.cloned())?;
let indent = LineIndent::from_iter(
matcher
@@ -486,18 +456,10 @@ impl EditAgent {
.unwrap_or(&String::new())
.chars(),
);
let resolved_old_texts = if let Some(best_match) = best_match {
vec![ResolvedOldText {
range: best_match,
indent,
}]
} else {
matches
.into_iter()
.map(|range| ResolvedOldText { range, indent })
.collect::<Vec<_>>()
};
let resolved_old_texts = matches
.into_iter()
.map(|range| ResolvedOldText { range, indent })
.collect::<Vec<_>>();
Ok((edit_events, resolved_old_texts))
});
@@ -1379,13 +1341,7 @@ mod tests {
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
let model = Arc::new(FakeLanguageModel::default());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
EditAgent::new(
model,
project,
action_log,
Templates::new(),
EditFormat::XmlTags,
)
EditAgent::new(model, project, action_log, Templates::new())
}
#[gpui::test(iterations = 10)]
@@ -1418,12 +1374,10 @@ mod tests {
&agent,
indoc! {"
<old_text>
return 42;
}
return 42;
</old_text>
<new_text>
return 100;
}
return 100;
</new_text>
"},
&mut rng,
@@ -1453,7 +1407,7 @@ mod tests {
// And AmbiguousEditRange even should be emitted
let events = drain_events(&mut events);
let ambiguous_ranges = vec![2..3, 6..7, 10..11];
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
assert!(
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
"Should emit AmbiguousEditRange for non-unique text"

View File

@@ -1,31 +1,18 @@
use anyhow::bail;
use derive_more::{Add, AddAssign};
use language_model::LanguageModel;
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{mem, ops::Range, str::FromStr, sync::Arc};
use std::{mem, ops::Range};
const OLD_TEXT_END_TAG: &str = "</old_text>";
const NEW_TEXT_END_TAG: &str = "</new_text>";
const EDITS_END_TAG: &str = "</edits>";
const SEARCH_MARKER: &str = "<<<<<<< SEARCH";
const SEPARATOR_MARKER: &str = "=======";
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
#[derive(Debug)]
pub enum EditParserEvent {
OldTextChunk {
chunk: String,
done: bool,
line_hint: Option<u32>,
},
NewTextChunk {
chunk: String,
done: bool,
},
OldTextChunk { chunk: String, done: bool },
NewTextChunk { chunk: String, done: bool },
}
#[derive(
@@ -36,164 +23,45 @@ pub struct EditParserMetrics {
pub mismatched_tags: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EditFormat {
/// XML-like tags:
/// <old_text>...</old_text>
/// <new_text>...</new_text>
XmlTags,
/// Diff-fenced format, in which:
/// - Text before the SEARCH marker is ignored
/// - Fences are optional
/// - Line hint is optional.
///
/// Example:
///
/// ```diff
/// <<<<<<< SEARCH line=42
/// ...
/// =======
/// ...
/// >>>>>>> REPLACE
/// ```
DiffFenced,
}
impl FromStr for EditFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
match s.to_lowercase().as_str() {
"xml_tags" | "xml" => Ok(EditFormat::XmlTags),
"diff_fenced" | "diff-fenced" | "diff" => Ok(EditFormat::DiffFenced),
_ => bail!("Unknown EditFormat: {}", s),
}
}
}
impl EditFormat {
/// Return an optimal edit format for the language model
pub fn from_model(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
if model.provider_id().0 == "google" {
Ok(EditFormat::DiffFenced)
} else {
Ok(EditFormat::XmlTags)
}
}
/// Return an optimal edit format for the language model,
/// with the ability to override it by setting the
/// `ZED_EDIT_FORMAT` environment variable
#[allow(dead_code)]
pub fn from_env(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
let default = EditFormat::from_model(model)?;
std::env::var("ZED_EDIT_FORMAT").map_or(Ok(default), |s| EditFormat::from_str(&s))
}
}
pub trait EditFormatParser: Send + std::fmt::Debug {
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]>;
fn take_metrics(&mut self) -> EditParserMetrics;
}
#[derive(Debug)]
pub struct XmlEditParser {
state: XmlParserState,
pub struct EditParser {
state: EditParserState,
buffer: String,
metrics: EditParserMetrics,
}
#[derive(Debug, PartialEq)]
enum XmlParserState {
enum EditParserState {
Pending,
WithinOldText { start: bool, line_hint: Option<u32> },
WithinOldText { start: bool },
AfterOldText,
WithinNewText { start: bool },
}
#[derive(Debug)]
pub struct DiffFencedEditParser {
state: DiffParserState,
buffer: String,
metrics: EditParserMetrics,
}
#[derive(Debug, PartialEq)]
enum DiffParserState {
Pending,
WithinSearch { start: bool, line_hint: Option<u32> },
WithinReplace { start: bool },
}
/// Main parser that delegates to format-specific parsers
pub struct EditParser {
parser: Box<dyn EditFormatParser>,
}
impl XmlEditParser {
impl EditParser {
pub fn new() -> Self {
XmlEditParser {
state: XmlParserState::Pending,
EditParser {
state: EditParserState::Pending,
buffer: String::new(),
metrics: EditParserMetrics::default(),
}
}
fn find_end_tag(&self) -> Option<Range<usize>> {
let (tag, start_ix) = END_TAGS
.iter()
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
.min_by_key(|(_, ix)| *ix)?;
Some(start_ix..start_ix + tag.len())
}
fn ends_with_tag_prefix(&self) -> bool {
let mut end_prefixes = END_TAGS
.iter()
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
.chain(["\n"]);
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
}
fn parse_line_hint(&self, tag: &str) -> Option<u32> {
use std::sync::LazyLock;
static LINE_HINT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
LINE_HINT_REGEX
.captures(tag)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse::<u32>().ok())
}
}
impl EditFormatParser for XmlEditParser {
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
self.buffer.push_str(chunk);
let mut edit_events = SmallVec::new();
loop {
match &mut self.state {
XmlParserState::Pending => {
if let Some(start) = self.buffer.find("<old_text") {
if let Some(tag_end) = self.buffer[start..].find('>') {
let tag_end = start + tag_end + 1;
let tag = &self.buffer[start..tag_end];
let line_hint = self.parse_line_hint(tag);
self.buffer.drain(..tag_end);
self.state = XmlParserState::WithinOldText {
start: true,
line_hint,
};
} else {
break;
}
EditParserState::Pending => {
if let Some(start) = self.buffer.find("<old_text>") {
self.buffer.drain(..start + "<old_text>".len());
self.state = EditParserState::WithinOldText { start: true };
} else {
break;
}
}
XmlParserState::WithinOldText { start, line_hint } => {
EditParserState::WithinOldText { start } => {
if !self.buffer.is_empty() {
if *start && self.buffer.starts_with('\n') {
self.buffer.remove(0);
@@ -201,7 +69,6 @@ impl EditFormatParser for XmlEditParser {
*start = false;
}
let line_hint = *line_hint;
if let Some(tag_range) = self.find_end_tag() {
let mut chunk = self.buffer[..tag_range.start].to_string();
if chunk.ends_with('\n') {
@@ -214,32 +81,27 @@ impl EditFormatParser for XmlEditParser {
}
self.buffer.drain(..tag_range.end);
self.state = XmlParserState::AfterOldText;
edit_events.push(EditParserEvent::OldTextChunk {
chunk,
done: true,
line_hint,
});
self.state = EditParserState::AfterOldText;
edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });
} else {
if !self.ends_with_tag_prefix() {
edit_events.push(EditParserEvent::OldTextChunk {
chunk: mem::take(&mut self.buffer),
done: false,
line_hint,
});
}
break;
}
}
XmlParserState::AfterOldText => {
EditParserState::AfterOldText => {
if let Some(start) = self.buffer.find("<new_text>") {
self.buffer.drain(..start + "<new_text>".len());
self.state = XmlParserState::WithinNewText { start: true };
self.state = EditParserState::WithinNewText { start: true };
} else {
break;
}
}
XmlParserState::WithinNewText { start } => {
EditParserState::WithinNewText { start } => {
if !self.buffer.is_empty() {
if *start && self.buffer.starts_with('\n') {
self.buffer.remove(0);
@@ -259,7 +121,7 @@ impl EditFormatParser for XmlEditParser {
}
self.buffer.drain(..tag_range.end);
self.state = XmlParserState::Pending;
self.state = EditParserState::Pending;
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
} else {
if !self.ends_with_tag_prefix() {
@@ -276,163 +138,24 @@ impl EditFormatParser for XmlEditParser {
edit_events
}
fn take_metrics(&mut self) -> EditParserMetrics {
std::mem::take(&mut self.metrics)
}
}
impl DiffFencedEditParser {
pub fn new() -> Self {
DiffFencedEditParser {
state: DiffParserState::Pending,
buffer: String::new(),
metrics: EditParserMetrics::default(),
}
}
fn ends_with_diff_marker_prefix(&self) -> bool {
let diff_markers = [SEPARATOR_MARKER, REPLACE_MARKER];
let mut diff_prefixes = diff_markers
fn find_end_tag(&self) -> Option<Range<usize>> {
let (tag, start_ix) = END_TAGS
.iter()
.flat_map(|marker| (1..marker.len()).map(move |i| &marker[..i]))
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
.min_by_key(|(_, ix)| *ix)?;
Some(start_ix..start_ix + tag.len())
}
fn ends_with_tag_prefix(&self) -> bool {
let mut end_prefixes = END_TAGS
.iter()
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
.chain(["\n"]);
diff_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
}
fn parse_line_hint(&self, search_line: &str) -> Option<u32> {
use regex::Regex;
use std::sync::LazyLock;
static LINE_HINT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
LINE_HINT_REGEX
.captures(search_line)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse::<u32>().ok())
}
}
impl EditFormatParser for DiffFencedEditParser {
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
self.buffer.push_str(chunk);
let mut edit_events = SmallVec::new();
loop {
match &mut self.state {
DiffParserState::Pending => {
if let Some(diff) = self.buffer.find(SEARCH_MARKER) {
let search_end = diff + SEARCH_MARKER.len();
if let Some(newline_pos) = self.buffer[search_end..].find('\n') {
let search_line = &self.buffer[diff..search_end + newline_pos];
let line_hint = self.parse_line_hint(search_line);
self.buffer.drain(..search_end + newline_pos + 1);
self.state = DiffParserState::WithinSearch {
start: true,
line_hint,
};
} else {
break;
}
} else {
break;
}
}
DiffParserState::WithinSearch { start, line_hint } => {
if !self.buffer.is_empty() {
if *start && self.buffer.starts_with('\n') {
self.buffer.remove(0);
}
*start = false;
}
let line_hint = *line_hint;
if let Some(separator_pos) = self.buffer.find(SEPARATOR_MARKER) {
let mut chunk = self.buffer[..separator_pos].to_string();
if chunk.ends_with('\n') {
chunk.pop();
}
let separator_end = separator_pos + SEPARATOR_MARKER.len();
if let Some(newline_pos) = self.buffer[separator_end..].find('\n') {
self.buffer.drain(..separator_end + newline_pos + 1);
self.state = DiffParserState::WithinReplace { start: true };
edit_events.push(EditParserEvent::OldTextChunk {
chunk,
done: true,
line_hint,
});
} else {
break;
}
} else {
if !self.ends_with_diff_marker_prefix() {
edit_events.push(EditParserEvent::OldTextChunk {
chunk: mem::take(&mut self.buffer),
done: false,
line_hint,
});
}
break;
}
}
DiffParserState::WithinReplace { start } => {
if !self.buffer.is_empty() {
if *start && self.buffer.starts_with('\n') {
self.buffer.remove(0);
}
*start = false;
}
if let Some(replace_pos) = self.buffer.find(REPLACE_MARKER) {
let mut chunk = self.buffer[..replace_pos].to_string();
if chunk.ends_with('\n') {
chunk.pop();
}
self.buffer.drain(..replace_pos + REPLACE_MARKER.len());
if let Some(newline_pos) = self.buffer.find('\n') {
self.buffer.drain(..newline_pos + 1);
} else {
self.buffer.clear();
}
self.state = DiffParserState::Pending;
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
} else {
if !self.ends_with_diff_marker_prefix() {
edit_events.push(EditParserEvent::NewTextChunk {
chunk: mem::take(&mut self.buffer),
done: false,
});
}
break;
}
}
}
}
edit_events
}
fn take_metrics(&mut self) -> EditParserMetrics {
std::mem::take(&mut self.metrics)
}
}
impl EditParser {
pub fn new(format: EditFormat) -> Self {
let parser: Box<dyn EditFormatParser> = match format {
EditFormat::XmlTags => Box::new(XmlEditParser::new()),
EditFormat::DiffFenced => Box::new(DiffFencedEditParser::new()),
};
EditParser { parser }
}
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
self.parser.push(chunk)
}
pub fn finish(mut self) -> EditParserMetrics {
self.parser.take_metrics()
pub fn finish(self) -> EditParserMetrics {
self.metrics
}
}
@@ -444,8 +167,8 @@ mod tests {
use std::cmp;
#[gpui::test(iterations = 1000)]
fn test_xml_single_edit(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_single_edit(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
"<old_text>original</old_text><new_text>updated</new_text>",
@@ -455,7 +178,6 @@ mod tests {
vec![Edit {
old_text: "original".to_string(),
new_text: "updated".to_string(),
line_hint: None,
}]
);
assert_eq!(
@@ -468,8 +190,8 @@ mod tests {
}
#[gpui::test(iterations = 1000)]
fn test_xml_multiple_edits(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_multiple_edits(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
indoc! {"
@@ -487,12 +209,10 @@ mod tests {
Edit {
old_text: "first old".to_string(),
new_text: "first new".to_string(),
line_hint: None,
},
Edit {
old_text: "second old".to_string(),
new_text: "second new".to_string(),
line_hint: None,
},
]
);
@@ -506,8 +226,8 @@ mod tests {
}
#[gpui::test(iterations = 1000)]
fn test_xml_edits_with_extra_text(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_edits_with_extra_text(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
indoc! {"
@@ -524,17 +244,14 @@ mod tests {
Edit {
old_text: "content".to_string(),
new_text: "updated content".to_string(),
line_hint: None,
},
Edit {
old_text: "second item".to_string(),
new_text: "modified second item".to_string(),
line_hint: None,
},
Edit {
old_text: "third case".to_string(),
new_text: "improved third case".to_string(),
line_hint: None,
},
]
);
@@ -548,8 +265,8 @@ mod tests {
}
#[gpui::test(iterations = 1000)]
fn test_xml_nested_tags(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_nested_tags(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
"<old_text>code with <tag>nested</tag> elements</old_text><new_text>new <code>content</code></new_text>",
@@ -559,7 +276,6 @@ mod tests {
vec![Edit {
old_text: "code with <tag>nested</tag> elements".to_string(),
new_text: "new <code>content</code>".to_string(),
line_hint: None,
}]
);
assert_eq!(
@@ -572,8 +288,8 @@ mod tests {
}
#[gpui::test(iterations = 1000)]
fn test_xml_empty_old_and_new_text(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_empty_old_and_new_text(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
"<old_text></old_text><new_text></new_text>",
@@ -583,7 +299,6 @@ mod tests {
vec![Edit {
old_text: "".to_string(),
new_text: "".to_string(),
line_hint: None,
}]
);
assert_eq!(
@@ -596,8 +311,8 @@ mod tests {
}
#[gpui::test(iterations = 100)]
fn test_xml_multiline_content(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_multiline_content(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
"<old_text>line1\nline2\nline3</old_text><new_text>line1\nmodified line2\nline3</new_text>",
@@ -607,7 +322,6 @@ mod tests {
vec![Edit {
old_text: "line1\nline2\nline3".to_string(),
new_text: "line1\nmodified line2\nline3".to_string(),
line_hint: None,
}]
);
assert_eq!(
@@ -620,8 +334,8 @@ mod tests {
}
#[gpui::test(iterations = 1000)]
fn test_xml_mismatched_tags(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
fn test_mismatched_tags(mut rng: StdRng) {
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
// Reduced from an actual Sonnet 3.7 output
@@ -654,12 +368,10 @@ mod tests {
Edit {
old_text: "a\nb\nc".to_string(),
new_text: "a\nB\nc".to_string(),
line_hint: None,
},
Edit {
old_text: "d\ne\nf".to_string(),
new_text: "D\ne\nF".to_string(),
line_hint: None,
}
]
);
@@ -671,7 +383,7 @@ mod tests {
}
);
let mut parser = EditParser::new(EditFormat::XmlTags);
let mut parser = EditParser::new();
assert_eq!(
parse_random_chunks(
// Reduced from an actual Opus 4 output
@@ -690,7 +402,6 @@ mod tests {
vec![Edit {
old_text: "Lorem".to_string(),
new_text: "LOREM".to_string(),
line_hint: None,
},]
);
assert_eq!(
@@ -702,297 +413,10 @@ mod tests {
);
}
#[gpui::test(iterations = 1000)]
fn test_diff_fenced_single_edit(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::DiffFenced);
assert_eq!(
parse_random_chunks(
indoc! {"
<<<<<<< SEARCH
original text
=======
updated text
>>>>>>> REPLACE
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "original text".to_string(),
new_text: "updated text".to_string(),
line_hint: None,
}]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 0,
mismatched_tags: 0
}
);
}
#[gpui::test(iterations = 100)]
fn test_diff_fenced_with_markdown_fences(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::DiffFenced);
assert_eq!(
parse_random_chunks(
indoc! {"
```diff
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "from flask import Flask".to_string(),
new_text: "import math\nfrom flask import Flask".to_string(),
line_hint: None,
}]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 0,
mismatched_tags: 0
}
);
}
#[gpui::test(iterations = 100)]
fn test_diff_fenced_multiple_edits(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::DiffFenced);
assert_eq!(
parse_random_chunks(
indoc! {"
<<<<<<< SEARCH
first old
=======
first new
>>>>>>> REPLACE
<<<<<<< SEARCH
second old
=======
second new
>>>>>>> REPLACE
"},
&mut parser,
&mut rng
),
vec![
Edit {
old_text: "first old".to_string(),
new_text: "first new".to_string(),
line_hint: None,
},
Edit {
old_text: "second old".to_string(),
new_text: "second new".to_string(),
line_hint: None,
},
]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 0,
mismatched_tags: 0
}
);
}
#[gpui::test(iterations = 100)]
fn test_mixed_formats(mut rng: StdRng) {
// Test XML format parser only parses XML tags
let mut xml_parser = EditParser::new(EditFormat::XmlTags);
assert_eq!(
parse_random_chunks(
indoc! {"
<old_text>xml style old</old_text><new_text>xml style new</new_text>
<<<<<<< SEARCH
diff style old
=======
diff style new
>>>>>>> REPLACE
"},
&mut xml_parser,
&mut rng
),
vec![Edit {
old_text: "xml style old".to_string(),
new_text: "xml style new".to_string(),
line_hint: None,
},]
);
assert_eq!(
xml_parser.finish(),
EditParserMetrics {
tags: 2,
mismatched_tags: 0
}
);
// Test diff-fenced format parser only parses diff markers
let mut diff_parser = EditParser::new(EditFormat::DiffFenced);
assert_eq!(
parse_random_chunks(
indoc! {"
<old_text>xml style old</old_text><new_text>xml style new</new_text>
<<<<<<< SEARCH
diff style old
=======
diff style new
>>>>>>> REPLACE
"},
&mut diff_parser,
&mut rng
),
vec![Edit {
old_text: "diff style old".to_string(),
new_text: "diff style new".to_string(),
line_hint: None,
},]
);
assert_eq!(
diff_parser.finish(),
EditParserMetrics {
tags: 0,
mismatched_tags: 0
}
);
}
#[gpui::test(iterations = 100)]
fn test_diff_fenced_empty_sections(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::DiffFenced);
assert_eq!(
parse_random_chunks(
indoc! {"
<<<<<<< SEARCH
=======
>>>>>>> REPLACE
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "".to_string(),
new_text: "".to_string(),
line_hint: None,
}]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 0,
mismatched_tags: 0
}
);
}
#[gpui::test(iterations = 100)]
fn test_diff_fenced_with_line_hint(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::DiffFenced);
let edits = parse_random_chunks(
indoc! {"
<<<<<<< SEARCH line=42
original text
=======
updated text
>>>>>>> REPLACE
"},
&mut parser,
&mut rng,
);
assert_eq!(
edits,
vec![Edit {
old_text: "original text".to_string(),
line_hint: Some(42),
new_text: "updated text".to_string(),
}]
);
}
#[gpui::test(iterations = 100)]
fn test_xml_line_hints(mut rng: StdRng) {
// Line hint is a single quoted line number
let mut parser = EditParser::new(EditFormat::XmlTags);
let edits = parse_random_chunks(
r#"
<old_text line="23">original code</old_text>
<new_text>updated code</new_text>"#,
&mut parser,
&mut rng,
);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].old_text, "original code");
assert_eq!(edits[0].line_hint, Some(23));
assert_eq!(edits[0].new_text, "updated code");
// Line hint is a single unquoted line number
let mut parser = EditParser::new(EditFormat::XmlTags);
let edits = parse_random_chunks(
r#"
<old_text line=45>original code</old_text>
<new_text>updated code</new_text>"#,
&mut parser,
&mut rng,
);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].old_text, "original code");
assert_eq!(edits[0].line_hint, Some(45));
assert_eq!(edits[0].new_text, "updated code");
// Line hint is a range
let mut parser = EditParser::new(EditFormat::XmlTags);
let edits = parse_random_chunks(
r#"
<old_text line="23:50">original code</old_text>
<new_text>updated code</new_text>"#,
&mut parser,
&mut rng,
);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].old_text, "original code");
assert_eq!(edits[0].line_hint, Some(23));
assert_eq!(edits[0].new_text, "updated code");
// No line hint
let mut parser = EditParser::new(EditFormat::XmlTags);
let edits = parse_random_chunks(
r#"
<old_text>old</old_text>
<new_text>new</new_text>"#,
&mut parser,
&mut rng,
);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].old_text, "old");
assert_eq!(edits[0].line_hint, None);
assert_eq!(edits[0].new_text, "new");
}
#[derive(Default, Debug, PartialEq, Eq)]
struct Edit {
old_text: String,
new_text: String,
line_hint: Option<u32>,
}
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
@@ -1009,15 +433,10 @@ mod tests {
for chunk_ix in chunk_indices {
for event in parser.push(&input[last_ix..chunk_ix]) {
match event {
EditParserEvent::OldTextChunk {
chunk,
done,
line_hint,
} => {
EditParserEvent::OldTextChunk { chunk, done } => {
old_text.as_mut().unwrap().push_str(&chunk);
if done {
pending_edit.old_text = old_text.take().unwrap();
pending_edit.line_hint = line_hint;
new_text = Some(String::new());
}
}

View File

@@ -26,7 +26,6 @@ use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
path::Path,
str::FromStr,
sync::mpsc,
};
@@ -39,11 +38,10 @@ fn eval_extract_handle_command_output() {
//
// Model | Pass rate
// ----------------------------|----------
// claude-3.7-sonnet | 0.99 (2025-06-14)
// claude-sonnet-4 | 0.97 (2025-06-14)
// gemini-2.5-pro-06-05 | 0.98 (2025-06-16)
// gemini-2.5-flash | 0.11 (2025-05-22)
// gpt-4.1 | 1.00 (2025-05-22)
// claude-3.7-sonnet | 0.98
// gemini-2.5-pro-06-05 | 0.77
// gemini-2.5-flash | 0.11
// gpt-4.1 | 1.00
let input_file_path = "root/blame.rs";
let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
@@ -59,7 +57,7 @@ fn eval_extract_handle_command_output() {
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
eval(
100,
0.95,
0.7, // Taking the lower bar for Gemini
0.05,
EvalInput::from_conversation(
vec![
@@ -112,13 +110,6 @@ fn eval_extract_handle_command_output() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_delete_run_git_blame() {
// Model | Pass rate
// ----------------------------|----------
// claude-3.7-sonnet | 1.0 (2025-06-14)
// claude-sonnet-4 | 0.96 (2025-06-14)
// gemini-2.5-pro-06-05 | 1.0 (2025-06-16)
// gemini-2.5-flash |
// gpt-4.1 |
let input_file_path = "root/blame.rs";
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
@@ -174,12 +165,13 @@ fn eval_delete_run_git_blame() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_translate_doc_comments() {
// Results for 2025-05-22
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 1.0 (2025-06-14)
// claude-sonnet-4 | 1.0 (2025-06-14)
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
// claude-3.7-sonnet |
// gemini-2.5-pro-preview-03-25 | 1.0
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
let input_file_path = "root/canvas.rs";
@@ -236,12 +228,13 @@ fn eval_translate_doc_comments() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
// Results for 2025-05-22
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 0.96 (2025-06-14)
// claude-sonnet-4 | 0.11 (2025-06-14)
// gemini-2.5-pro-preview-latest | 0.99 (2025-06-16)
// claude-3.7-sonnet | 0.98
// gemini-2.5-pro-preview-03-25 | 0.99
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
let input_file_path = "root/lib.rs";
@@ -361,12 +354,13 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_disable_cursor_blinking() {
// Results for 2025-05-22
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 0.99 (2025-06-14)
// claude-sonnet-4 | 0.85 (2025-06-14)
// gemini-2.5-pro-preview-latest | 0.97 (2025-06-16)
// claude-3.7-sonnet |
// gemini-2.5-pro-preview-03-25 | 1.0
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
let input_file_path = "root/editor.rs";
@@ -444,20 +438,14 @@ fn eval_disable_cursor_blinking() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_from_pixels_constructor() {
// Results for 2025-06-13
// Results for 2025-05-22
//
// The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE
// value. Higher values improve the pass rate but may sometimes cause
// edits to be misapplied. In the context of this eval, this means
// the agent might add from_pixels tests in incorrect locations
// (e.g., at the beginning of the file), yet the evaluation may still
// rate it highly.
// Model | Pass rate
// ============================================
//
// Model | Date | Pass rate
// =========================================================
// claude-4.0-sonnet | 2025-06-14 | 0.99
// claude-3.7-sonnet | 2025-06-14 | 0.88
// gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98
// claude-3.7-sonnet |
// gemini-2.5-pro-preview-03-25 | 0.94
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
@@ -467,7 +455,7 @@ fn eval_from_pixels_constructor() {
0.95,
// For whatever reason, this eval produces more mismatched tags.
// Increasing for now, let's see if we can bring this down.
0.25,
0.2,
EvalInput::from_conversation(
vec![
message(
@@ -653,14 +641,15 @@ fn eval_from_pixels_constructor() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_zode() {
// Results for 2025-05-22
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 1.0 (2025-06-14)
// claude-sonnet-4 | 1.0 (2025-06-14)
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
// gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
// gpt-4.1 | 1.0 (2025-05-22)
// claude-3.7-sonnet | 1.0
// gemini-2.5-pro-preview-03-25 | 1.0
// gemini-2.5-flash-preview-04-17 | 1.0
// gpt-4.1 | 1.0
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
@@ -759,12 +748,13 @@ fn eval_zode() {
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_add_overwrite_test() {
// Results for 2025-05-22
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 0.65 (2025-06-14)
// claude-sonnet-4 | 0.07 (2025-06-14)
// gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
// claude-3.7-sonnet | 0.16
// gemini-2.5-pro-preview-03-25 | 0.35
// gemini-2.5-flash-preview-04-17 |
// gpt-4.1 |
let input_file_path = "root/action_log.rs";
@@ -994,14 +984,15 @@ fn eval_create_empty_file() {
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
// Results for 2025-05-21:
//
// Model | Pass rate
// ============================================
//
// claude-3.7-sonnet | 1.00 (2025-06-14)
// claude-sonnet-4 | 1.00 (2025-06-14)
// gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21)
// gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21)
// gpt-4.1 | 1.00 (2025-05-21)
// claude-3.7-sonnet | 1.00
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// gpt-4.1 | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
@@ -1497,16 +1488,8 @@ impl EditAgentTest {
.await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let edit_format = EditFormat::from_env(agent_model.clone()).unwrap();
Self {
agent: EditAgent::new(
agent_model,
project.clone(),
action_log,
Templates::new(),
edit_format,
),
agent: EditAgent::new(agent_model, project.clone(), action_log, Templates::new()),
project,
judge_model,
}
@@ -1566,7 +1549,6 @@ impl EditAgentTest {
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
abs_path: Path::new("/path/to/root").into(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
@@ -1652,20 +1634,15 @@ impl EditAgentTest {
}
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
let mut attempt = 0;
loop {
attempt += 1;
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match err {
LanguageModelCompletionError::RateLimit(duration) => {
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
let jitter = duration.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: Rate limit exceeded. Retry after {duration:?} + jitter of {jitter:?}"
);
Timer::after(duration + jitter).await;
// Wait until after we are allowed to try again
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
Timer::after(duration).await;
continue;
}
_ => return Err(err.into()),

View File

@@ -10,9 +10,8 @@ const DELETION_COST: u32 = 10;
pub struct StreamingFuzzyMatcher {
snapshot: TextBufferSnapshot,
query_lines: Vec<String>,
line_hint: Option<u32>,
incomplete_line: String,
matches: Vec<Range<usize>>,
best_matches: Vec<Range<usize>>,
matrix: SearchMatrix,
}
@@ -22,9 +21,8 @@ impl StreamingFuzzyMatcher {
Self {
snapshot,
query_lines: Vec::new(),
line_hint: None,
incomplete_line: String::new(),
matches: Vec::new(),
best_matches: Vec::new(),
matrix: SearchMatrix::new(buffer_line_count + 1),
}
}
@@ -43,14 +41,9 @@ impl StreamingFuzzyMatcher {
///
/// Returns `Some(range)` if a match has been found with the accumulated
/// query so far, or `None` if no suitable match exists yet.
pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> {
if line_hint.is_some() {
self.line_hint = line_hint;
}
pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {
// Add the chunk to our incomplete line buffer
self.incomplete_line.push_str(chunk);
self.line_hint = line_hint;
if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
let complete_part = &self.incomplete_line[..=last_pos];
@@ -62,11 +55,20 @@ impl StreamingFuzzyMatcher {
self.incomplete_line.replace_range(..last_pos + 1, "");
self.matches = self.resolve_location_fuzzy();
}
self.best_matches = self.resolve_location_fuzzy();
let best_match = self.select_best_match();
best_match.or_else(|| self.matches.first().cloned())
if let Some(first_match) = self.best_matches.first() {
Some(first_match.clone())
} else {
None
}
} else {
if let Some(first_match) = self.best_matches.first() {
Some(first_match.clone())
} else {
None
}
}
}
/// Finish processing and return the final best match(es).
@@ -78,9 +80,9 @@ impl StreamingFuzzyMatcher {
if !self.incomplete_line.is_empty() {
self.query_lines.push(self.incomplete_line.clone());
self.incomplete_line.clear();
self.matches = self.resolve_location_fuzzy();
self.best_matches = self.resolve_location_fuzzy();
}
self.matches.clone()
self.best_matches.clone()
}
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
@@ -196,43 +198,6 @@ impl StreamingFuzzyMatcher {
valid_matches.into_iter().map(|(_, range)| range).collect()
}
/// Return the best match with starting position close enough to line_hint.
pub fn select_best_match(&self) -> Option<Range<usize>> {
// Allow line hint to be off by that many lines.
// Higher values increase probability of applying edits to a wrong place,
// Lower values increase edits failures and overall conversation length.
const LINE_HINT_TOLERANCE: u32 = 200;
if self.matches.is_empty() {
return None;
}
if self.matches.len() == 1 {
return self.matches.first().cloned();
}
let Some(line_hint) = self.line_hint else {
// Multiple ambiguous matches
return None;
};
let mut best_match = None;
let mut best_distance = u32::MAX;
for range in &self.matches {
let start_point = self.snapshot.offset_to_point(range.start);
let start_line = start_point.row;
let distance = start_line.abs_diff(line_hint);
if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
best_distance = distance;
best_match = Some(range.clone());
}
}
best_match
}
}
fn fuzzy_eq(left: &str, right: &str) -> bool {
@@ -675,52 +640,6 @@ mod tests {
);
}
#[gpui::test]
fn test_line_hint_selection() {
let text = indoc! {r#"
fn first_function() {
return 42;
}
fn second_function() {
return 42;
}
fn third_function() {
return 42;
}
"#};
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string());
let snapshot = buffer.snapshot();
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
// Given a query that matches all three functions
let query = "return 42;\n";
// Test with line hint pointing to second function (around line 5)
let best_match = matcher.push(query, Some(5)).expect("Failed to match query");
let matched_text = snapshot
.text_for_range(best_match.clone())
.collect::<String>();
assert!(matched_text.contains("return 42;"));
assert_eq!(
best_match,
63..77,
"Expected to match `second_function` based on the line hint"
);
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
matcher.push(query, None);
matcher.finish();
let best_match = matcher.select_best_match();
assert!(
best_match.is_none(),
"Best match should be None when query cannot be uniquely resolved"
);
}
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
@@ -734,7 +653,7 @@ mod tests {
// Push chunks incrementally
for chunk in &chunks {
matcher.push(chunk, None);
matcher.push(chunk);
}
let actual_ranges = matcher.finish();
@@ -787,7 +706,7 @@ mod tests {
fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
finder
.push(chunk, None)
.push(chunk)
.map(|range| finder.snapshot.text_for_range(range).collect::<String>())
}

View File

@@ -1,6 +1,6 @@
use crate::{
Templates,
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
@@ -10,7 +10,7 @@ use assistant_tool::{
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -69,13 +69,13 @@ pub struct EditFileToolInput {
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - /a/b/backend
/// - /c/d/frontend
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with `backend`. Without that, the path
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
@@ -201,14 +201,8 @@ impl Tool for EditFileTool {
let card_clone = card.clone();
let action_log_clone = action_log.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let edit_format = EditFormat::from_model(model.clone())?;
let edit_agent = EditAgent::new(
model,
project.clone(),
action_log_clone,
Templates::new(),
edit_format,
);
let edit_agent =
EditAgent::new(model, project.clone(), action_log_clone, Templates::new());
let buffer = project
.update(cx, |project, cx| {
@@ -339,18 +333,14 @@ impl Tool for EditFileTool {
);
anyhow::ensure!(
ambiguous_ranges.is_empty(),
{
let line_numbers = ambiguous_ranges
.iter()
.map(|range| range.start.to_string())
.collect::<Vec<_>>()
.join(", ");
formatdoc! {"
<old_text> matches more than one position in the file (lines: {line_numbers}). Read the
relevant sections of {input_path} again and extend <old_text> so
that I can perform the requested edits.
"}
}
// TODO: Include ambiguous_ranges, converted to line numbers.
// This would work best if we add `line_hint` parameter
// to edit_file_tool
formatdoc! {"
<old_text> matches more than one position in the file. Read the
relevant sections of {input_path} again and extend <old_text> so
that I can perform the requested edits.
"}
);
Ok(ToolResultOutput {
content: ToolResultContent::Text("No edits were made.".into()),
@@ -810,30 +800,11 @@ impl ToolCard for EditFileToolCard {
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
let snapshot =
editor.buffer().read(cx).snapshot(cx);
let first_hunk = editor
.diff_hunks_in_ranges(
&[editor::Anchor::min()
..editor::Anchor::max()],
&snapshot,
)
.next();
if let Some(first_hunk) = first_hunk {
let first_hunk_start =
first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::fit()),
window,
cx,
|selections| {
selections.select_anchor_ranges([
first_hunk_start
..first_hunk_start,
]);
},
)
}
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}

View File

@@ -31,8 +31,8 @@ pub struct ReadFileToolInput {
/// <example>
/// If the project has the following root directories:
///
/// - /a/b/directory1
/// - /c/d/directory2
/// - directory1
/// - directory2
///
/// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.

View File

@@ -3,21 +3,21 @@ You MUST respond with a series of edits to a file, using the following format:
```
<edits>
<old_text line=10>
<old_text>
OLD TEXT 1 HERE
</old_text>
<new_text>
NEW TEXT 1 HERE
</new_text>
<old_text line=456>
<old_text>
OLD TEXT 2 HERE
</old_text>
<new_text>
NEW TEXT 2 HERE
</new_text>
<old_text line=42>
<old_text>
OLD TEXT 3 HERE
</old_text>
<new_text>
@@ -33,7 +33,6 @@ NEW TEXT 3 HERE
- `<old_text>` must exactly match existing file content, including indentation
- `<old_text>` must come from the actual file, not an outline
- `<old_text>` cannot be empty
- `line` should be a starting line number for the text to be replaced
- Be minimal with replacements:
- For unique lines, include only those lines
- For non-unique lines, include enough context to identify them
@@ -49,7 +48,7 @@ Claude and gpt-4.1 don't really need it. --}}
<example>
<edits>
<old_text line=3>
<old_text>
struct User {
name: String,
email: String,
@@ -63,7 +62,7 @@ struct User {
}
</new_text>
<old_text line=25>
<old_text>
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),

View File

@@ -1,77 +0,0 @@
You MUST respond with a series of edits to a file, using the following diff format:
```
<<<<<<< SEARCH line=1
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
<<<<<<< SEARCH line=325
return 0
=======
print("Done")
return 0
>>>>>>> REPLACE
```
# File Editing Instructions
- Use the SEARCH/REPLACE diff format shown above
- The SEARCH section must exactly match existing file content, including indentation
- The SEARCH section must come from the actual file, not an outline
- The SEARCH section cannot be empty
- `line` should be a starting line number for the text to be replaced
- Be minimal with replacements:
- For unique lines, include only those lines
- For non-unique lines, include enough context to identify them
- Do not escape quotes, newlines, or other characters
- For multiple occurrences, repeat the same diff block for each instance
- Edits are sequential - each assumes previous edits are already applied
- Only edit the specified file
# Example
```
<<<<<<< SEARCH line=3
struct User {
name: String,
email: String,
}
=======
struct User {
name: String,
email: String,
active: bool,
}
>>>>>>> REPLACE
<<<<<<< SEARCH line=25
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
};
=======
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
active: true,
};
>>>>>>> REPLACE
```
# Final instructions
Tool calls have been disabled. You MUST respond using the SEARCH/REPLACE diff format only.
<file_to_edit>
{{path}}
</file_to_edit>
<edit_description>
{{edit_description}}
</edit_description>

View File

@@ -221,7 +221,7 @@ pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(window.prompt(
gpui::PromptLevel::Info,
@@ -296,11 +296,6 @@ impl InstallerDir {
}
}
pub enum UpdateCheckType {
Automatic,
Manual,
}
impl AutoUpdater {
pub fn get(cx: &mut App) -> Option<Entity<Self>> {
cx.default_global::<GlobalAutoUpdate>().0.clone()
@@ -318,13 +313,13 @@ impl AutoUpdater {
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
loop {
this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
this.update(cx, |this, cx| this.poll(cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
pub fn poll(&mut self, cx: &mut Context<Self>) {
if self.pending_poll.is_some() {
return;
}
@@ -336,18 +331,8 @@ impl AutoUpdater {
this.update(cx, |this, cx| {
this.pending_poll = None;
if let Err(error) = result {
this.status = match check_type {
// Be quiet if the check was automated (e.g. when offline)
UpdateCheckType::Automatic => {
log::info!("auto-update check failed: error:{:?}", error);
AutoUpdateStatus::Idle
}
UpdateCheckType::Manual => {
log::error!("auto-update failed: error:{:?}", error);
AutoUpdateStatus::Errored
}
};
log::error!("auto-update failed: error:{:?}", error);
this.status = AutoUpdateStatus::Errored;
cx.notify();
}
})

View File

@@ -269,6 +269,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
name: None,
email: None,
}],
},
);
@@ -322,6 +323,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
name: None,
email: None,
}],
},
);
@@ -366,6 +368,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "as-cii".into(),
avatar_url: "http://avatar.com/as-cii".into(),
name: None,
email: None,
}],
},
);

View File

@@ -127,9 +127,6 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
}
fn main() -> Result<()> {
#[cfg(unix)]
util::prevent_root_execution();
// Exit flatpak sandbox if needed
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{

View File

@@ -28,9 +28,6 @@ feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
# Don't update `hickory-resolver`, it has a bug that causes it to not resolve DNS queries correctly.
# See https://github.com/hickory-dns/hickory-dns/issues/3048
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
http_client.workspace = true
http_client_tls.workspace = true
httparse = "1.10"
@@ -39,7 +36,6 @@ paths.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
regex.workspace = true
release_channel.workspace = true
rpc = { workspace = true, features = ["gpui"] }
schemars.workspace = true
@@ -52,7 +48,7 @@ telemetry_events.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http.workspace = true
tiny_http = "0.8"
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
url.workspace = true
util.workspace = true
@@ -64,12 +60,11 @@ workspace-hack.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
fs.workspace = true
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View File

@@ -1887,16 +1887,8 @@ mod tests {
.set_entity(&entity3, &mut cx.to_async());
drop(subscription3);
server.send(proto::JoinProject {
project_id: 1,
committer_name: None,
committer_email: None,
});
server.send(proto::JoinProject {
project_id: 2,
committer_name: None,
committer_email: None,
});
server.send(proto::JoinProject { project_id: 1 });
server.send(proto::JoinProject { project_id: 2 });
done_rx1.recv().await.unwrap();
done_rx2.recv().await.unwrap();
}

View File

@@ -3,30 +3,20 @@
mod http_proxy;
mod socks_proxy;
use std::sync::LazyLock;
use anyhow::{Context as _, Result};
use hickory_resolver::{
AsyncResolver, TokioAsyncResolver,
config::LookupIpStrategy,
name_server::{GenericConnector, TokioRuntimeProvider},
system_conf,
};
use http_client::Url;
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
use tokio_socks::{IntoTargetAddr, TargetAddr};
use util::ResultExt;
pub(crate) async fn connect_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy).await else {
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
anyhow::bail!("Parsing proxy url type failed");
anyhow::bail!("Parsing proxy url failed");
};
// Connect to proxy and wrap protocol later
@@ -49,8 +39,10 @@ enum ProxyType<'t> {
HttpProxy(HttpProxyType<'t>),
}
async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
let proxy_type = match scheme {
scheme if scheme.starts_with("socks") => {
Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
@@ -60,38 +52,8 @@ async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)>
}
_ => None,
}?;
let (ip, port) = {
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
resolve_proxy_url_if_needed((host, port)).await.log_err()?
};
Some(((ip, port), proxy_type))
}
static SYSTEM_DNS_RESOLVER: LazyLock<AsyncResolver<GenericConnector<TokioRuntimeProvider>>> =
LazyLock::new(|| {
let (config, mut opts) = system_conf::read_system_conf().unwrap();
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
TokioAsyncResolver::tokio(config, opts)
});
async fn resolve_proxy_url_if_needed(proxy: (String, u16)) -> Result<(String, u16)> {
let proxy = proxy
.into_target_addr()
.context("Failed to parse proxy addr")?;
match proxy {
TargetAddr::Domain(domain, port) => {
let ip = SYSTEM_DNS_RESOLVER
.lookup_ip(domain.as_ref())
.await?
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No IP found for proxy domain {domain}"))?;
Ok((ip.to_string(), port))
}
TargetAddr::Ip(ip_addr) => Ok((ip_addr.ip().to_string(), ip_addr.port())),
}
Some(((host, port), proxy_type))
}
pub(crate) trait AsyncReadWrite:

View File

@@ -1,7 +1,5 @@
//! socks proxy
use std::net::SocketAddr;
use anyhow::{Context as _, Result};
use http_client::Url;
use tokio::net::TcpStream;
@@ -10,8 +8,6 @@ use tokio_socks::{
tcp::{Socks4Stream, Socks5Stream},
};
use crate::proxy::SYSTEM_DNS_RESOLVER;
use super::AsyncReadWrite;
/// Identification to a Socks V4 Proxy
@@ -77,14 +73,12 @@ pub(super) async fn connect_socks_proxy_stream(
};
let rpc_host = match (rpc_host, local_dns) {
(TargetAddr::Domain(domain, port), true) => {
let ip_addr = SYSTEM_DNS_RESOLVER
.lookup_ip(domain.as_ref())
let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
.await
.with_context(|| format!("Failed to lookup domain {}", domain))?
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
TargetAddr::Ip(SocketAddr::new(ip_addr, port))
TargetAddr::Ip(ip_addr)
}
(rpc_host, _) => rpc_host,
};

View File

@@ -8,11 +8,10 @@ use futures::{Future, FutureExt, StreamExt};
use gpui::{App, AppContext as _, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use parking_lot::Mutex;
use regex::Regex;
use release_channel::ReleaseChannel;
use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Write;
use std::sync::LazyLock;
@@ -46,13 +45,31 @@ struct TelemetryState {
first_event_date_time: Option<Instant>,
event_coalescer: EventCoalescer,
max_queue_size: usize,
worktrees_with_project_type_events_sent: HashSet<WorktreeId>,
worktree_id_map: WorktreeIdMap,
os_name: String,
app_version: String,
os_version: Option<String>,
}
#[derive(Debug)]
struct WorktreeIdMap(HashMap<String, ProjectCache>);
#[derive(Debug)]
struct ProjectCache {
name: String,
worktree_ids_reported: HashSet<WorktreeId>,
}
impl ProjectCache {
fn new(name: String) -> Self {
Self {
name,
worktree_ids_reported: HashSet::default(),
}
}
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 5;
@@ -74,10 +91,6 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
})
});
static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
});
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
@@ -181,7 +194,20 @@ impl Telemetry {
first_event_date_time: None,
event_coalescer: EventCoalescer::new(clock.clone()),
max_queue_size: MAX_QUEUE_LEN,
worktrees_with_project_type_events_sent: HashSet::new(),
worktree_id_map: WorktreeIdMap(HashMap::from_iter([
(
"pnpm-lock.yaml".to_string(),
ProjectCache::new("pnpm".to_string()),
),
(
"yarn.lock".to_string(),
ProjectCache::new("yarn".to_string()),
),
(
"package.json".to_string(),
ProjectCache::new("node".to_string()),
),
])),
os_version: None,
os_name: os_name(),
@@ -345,14 +371,44 @@ impl Telemetry {
}
}
pub fn report_discovered_project_type_events(
pub fn report_discovered_project_events(
self: &Arc<Self>,
worktree_id: WorktreeId,
updated_entries_set: &UpdatedEntriesSet,
) {
let Some(project_type_names) = self.detect_project_types(worktree_id, updated_entries_set)
else {
return;
let project_type_names: Vec<String> = {
let mut state = self.state.lock();
state
.worktree_id_map
.0
.iter_mut()
.filter_map(|(project_file_name, project_type_telemetry)| {
if project_type_telemetry
.worktree_ids_reported
.contains(&worktree_id)
{
return None;
}
let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.map(|name_str| name_str == project_file_name)
.unwrap_or(false)
});
if !project_file_found {
return None;
}
project_type_telemetry
.worktree_ids_reported
.insert(worktree_id);
Some(project_type_telemetry.name.clone())
})
.collect()
};
for project_type_name in project_type_names {
@@ -360,49 +416,6 @@ impl Telemetry {
}
}
fn detect_project_types(
self: &Arc<Self>,
worktree_id: WorktreeId,
updated_entries_set: &UpdatedEntriesSet,
) -> Option<Vec<String>> {
let mut state = self.state.lock();
if state
.worktrees_with_project_type_events_sent
.contains(&worktree_id)
{
return None;
}
let mut project_types: HashSet<String> = HashSet::new();
for (path, _, _) in updated_entries_set.iter() {
let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
continue;
};
if file_name == "pnpm-lock.yaml" {
project_types.insert("pnpm".to_string());
} else if file_name == "yarn.lock" {
project_types.insert("yarn".to_string());
} else if file_name == "package.json" {
project_types.insert("node".to_string());
} else if DOTNET_PROJECT_FILES_REGEX.is_match(file_name) {
project_types.insert("dotnet".to_string());
}
}
if !project_types.is_empty() {
state
.worktrees_with_project_type_events_sent
.insert(worktree_id);
}
let mut project_names_vec: Vec<String> = project_types.into_iter().collect();
project_names_vec.sort();
Some(project_names_vec)
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();
// RUST_LOG=telemetry=trace to debug telemetry events
@@ -565,9 +578,7 @@ mod tests {
use clock::FakeSystemClock;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use std::collections::HashMap;
use telemetry_events::FlexibleEvent;
use worktree::{PathChange, ProjectEntryId, WorktreeId};
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
@@ -685,115 +696,6 @@ mod tests {
});
}
#[gpui::test]
fn test_project_discovery_does_not_double_report(cx: &mut gpui::TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
let worktree_id = 1;
// Scan of empty worktree finds nothing
test_project_discovery_helper(telemetry.clone(), vec![], Some(vec![]), worktree_id);
// Files added, second scan of worktree 1 finds project type
test_project_discovery_helper(
telemetry.clone(),
vec!["package.json"],
Some(vec!["node"]),
worktree_id,
);
// Third scan of worktree does not double report, as we already reported
test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id);
}
#[gpui::test]
fn test_pnpm_project_discovery(cx: &mut gpui::TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
test_project_discovery_helper(
telemetry.clone(),
vec!["package.json", "pnpm-lock.yaml"],
Some(vec!["node", "pnpm"]),
1,
);
}
#[gpui::test]
fn test_yarn_project_discovery(cx: &mut gpui::TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
test_project_discovery_helper(
telemetry.clone(),
vec!["package.json", "yarn.lock"],
Some(vec!["node", "yarn"]),
1,
);
}
#[gpui::test]
fn test_dotnet_project_discovery(cx: &mut gpui::TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
// Using different worktrees, as production code blocks from reporting a
// project type for the same worktree multiple times
test_project_discovery_helper(
telemetry.clone().clone(),
vec!["global.json"],
Some(vec!["dotnet"]),
1,
);
test_project_discovery_helper(
telemetry.clone(),
vec!["Directory.Build.props"],
Some(vec!["dotnet"]),
2,
);
test_project_discovery_helper(
telemetry.clone(),
vec!["file.csproj"],
Some(vec!["dotnet"]),
3,
);
test_project_discovery_helper(
telemetry.clone(),
vec!["file.fsproj"],
Some(vec!["dotnet"]),
4,
);
test_project_discovery_helper(
telemetry.clone(),
vec!["file.vbproj"],
Some(vec!["dotnet"]),
5,
);
test_project_discovery_helper(telemetry.clone(), vec!["file.sln"], Some(vec!["dotnet"]), 6);
// Each worktree should only send a single project type event, even when
// encountering multiple files associated with that project type
test_project_discovery_helper(
telemetry,
vec!["global.json", "Directory.Build.props"],
Some(vec!["dotnet"]),
7,
);
}
// TODO:
// Test settings
// Update FakeHTTPClient to keep track of the number of requests and assert on it
@@ -810,32 +712,4 @@ mod tests {
&& telemetry.state.lock().flush_events_task.is_none()
&& telemetry.state.lock().first_event_date_time.is_none()
}
fn test_project_discovery_helper(
telemetry: Arc<Telemetry>,
file_paths: Vec<&str>,
expected_project_types: Option<Vec<&str>>,
worktree_id_num: usize,
) {
let worktree_id = WorktreeId::from_usize(worktree_id_num);
let entries: Vec<_> = file_paths
.into_iter()
.enumerate()
.map(|(i, path)| {
(
Arc::from(std::path::Path::new(path)),
ProjectEntryId::from_proto(i as u64 + 1),
PathChange::Added,
)
})
.collect();
let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());
let detected_project_types = telemetry.detect_project_types(worktree_id, &updated_entries);
let expected_project_types =
expected_project_types.map(|types| types.iter().map(|&t| t.to_string()).collect());
assert_eq!(detected_project_types, expected_project_types);
}
}

View File

@@ -49,6 +49,7 @@ pub struct User {
pub github_login: String,
pub avatar_uri: SharedUri,
pub name: Option<String>,
pub email: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -57,8 +58,6 @@ pub struct Collaborator {
pub replica_id: ReplicaId,
pub user_id: UserId,
pub is_host: bool,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
}
impl PartialOrd for User {
@@ -882,6 +881,7 @@ impl User {
github_login: message.github_login,
avatar_uri: message.avatar_url.into(),
name: message.name,
email: message.email,
})
}
}
@@ -912,8 +912,6 @@ impl Collaborator {
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
is_host: message.is_host,
committer_name: message.committer_name,
committer_email: message.committer_email,
})
}
}

View File

@@ -185,9 +185,7 @@ CREATE TABLE "project_collaborators" (
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
"is_host" BOOLEAN NOT NULL,
"committer_name" VARCHAR,
"committer_email" VARCHAR
"is_host" BOOLEAN NOT NULL
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");

View File

@@ -1,4 +0,0 @@
alter table project_collaborators
add column committer_name varchar;
alter table project_collaborators
add column committer_email varchar;

View File

@@ -97,7 +97,7 @@ impl std::fmt::Display for SystemIdHeader {
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new()
.route("/user", get(update_or_create_authenticated_user))
.route("/user", get(get_authenticated_user))
.route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
@@ -157,7 +157,7 @@ struct AuthenticatedUserResponse {
feature_flags: Vec<String>,
}
async fn update_or_create_authenticated_user(
async fn get_authenticated_user(
Query(params): Query<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<AuthenticatedUserResponse>> {
@@ -165,7 +165,7 @@ async fn update_or_create_authenticated_user(
let user = app
.db
.update_or_create_user_by_github_account(
.get_or_create_user_by_github_account(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),

View File

@@ -751,8 +751,6 @@ pub struct ProjectCollaborator {
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
}
impl ProjectCollaborator {
@@ -762,8 +760,6 @@ impl ProjectCollaborator {
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
is_host: self.is_host,
committer_name: self.committer_name.clone(),
committer_email: self.committer_email.clone(),
}
}
}

View File

@@ -118,8 +118,6 @@ impl Database {
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
committer_name: None,
committer_email: None,
})
.collect(),
})
@@ -227,8 +225,6 @@ impl Database {
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
committer_name: None,
committer_email: None,
})
.collect(),
},
@@ -265,8 +261,6 @@ impl Database {
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
is_host: false,
committer_name: None,
committer_email: None,
})
} else {
collaborator_ids_to_remove.push(db_collaborator.id);
@@ -396,8 +390,6 @@ impl Database {
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
is_host: false,
committer_name: None,
committer_email: None,
});
}

View File

@@ -739,6 +739,7 @@ impl Database {
),
github_login: user.github_login,
name: user.name,
email: user.email_address,
})
}
proto::ChannelMember {

View File

@@ -71,7 +71,7 @@ impl Database {
) -> Result<()> {
self.weak_transaction(|tx| async move {
let user = self
.update_or_create_user_by_github_account_tx(
.get_or_create_user_by_github_account_tx(
github_login,
github_user_id,
github_email,

View File

@@ -98,9 +98,7 @@ impl Database {
user_id: ActiveValue::set(participant.user_id),
replica_id: ActiveValue::set(ReplicaId(replica_id)),
is_host: ActiveValue::set(true),
id: ActiveValue::NotSet,
committer_name: ActiveValue::Set(None),
committer_email: ActiveValue::Set(None),
..Default::default()
}
.insert(&*tx)
.await?;
@@ -786,27 +784,13 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
user_id: UserId,
committer_name: Option<String>,
committer_email: Option<String>,
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
self.project_transaction(project_id, move |tx| {
let committer_name = committer_name.clone();
let committer_email = committer_email.clone();
async move {
let (project, role) = self
.access_project(project_id, connection, Capability::ReadOnly, &tx)
.await?;
self.join_project_internal(
project,
user_id,
committer_name,
committer_email,
connection,
role,
&tx,
)
self.project_transaction(project_id, |tx| async move {
let (project, role) = self
.access_project(project_id, connection, Capability::ReadOnly, &tx)
.await?;
self.join_project_internal(project, user_id, connection, role, &tx)
.await
}
})
.await
}
@@ -815,8 +799,6 @@ impl Database {
&self,
project: project::Model,
user_id: UserId,
committer_name: Option<String>,
committer_email: Option<String>,
connection: ConnectionId,
role: ChannelRole,
tx: &DatabaseTransaction,
@@ -840,9 +822,7 @@ impl Database {
user_id: ActiveValue::set(user_id),
replica_id: ActiveValue::set(replica_id),
is_host: ActiveValue::set(false),
id: ActiveValue::NotSet,
committer_name: ActiveValue::set(committer_name),
committer_email: ActiveValue::set(committer_email),
..Default::default()
}
.insert(tx)
.await?;
@@ -1046,8 +1026,6 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
committer_name: collaborator.committer_name,
committer_email: collaborator.committer_email,
})
.collect(),
worktrees,

View File

@@ -553,8 +553,6 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
committer_name: collaborator.committer_name.clone(),
committer_email: collaborator.committer_email.clone(),
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
@@ -859,8 +857,6 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
committer_name: collaborator.committer_name,
committer_email: collaborator.committer_email,
})
.collect::<Vec<_>>();

View File

@@ -111,7 +111,7 @@ impl Database {
.await
}
pub async fn update_or_create_user_by_github_account(
pub async fn get_or_create_user_by_github_account(
&self,
github_login: &str,
github_user_id: i32,
@@ -121,7 +121,7 @@ impl Database {
initial_channel_id: Option<ChannelId>,
) -> Result<User> {
self.transaction(|tx| async move {
self.update_or_create_user_by_github_account_tx(
self.get_or_create_user_by_github_account_tx(
github_login,
github_user_id,
github_email,
@@ -135,7 +135,7 @@ impl Database {
.await
}
pub async fn update_or_create_user_by_github_account_tx(
pub async fn get_or_create_user_by_github_account_tx(
&self,
github_login: &str,
github_user_id: i32,

View File

@@ -13,8 +13,6 @@ pub struct Model {
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
}
impl Model {

View File

@@ -126,16 +126,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: 0,
is_host: false,
committer_name: None,
committer_email: None,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: 1,
is_host: false,
committer_name: None,
committer_email: None,
}
]
);

View File

@@ -72,12 +72,12 @@ async fn test_get_users(db: &Arc<Database>) {
}
test_both_dbs!(
test_update_or_create_user_by_github_account,
test_update_or_create_user_by_github_account_postgres,
test_update_or_create_user_by_github_account_sqlite
test_get_or_create_user_by_github_account,
test_get_or_create_user_by_github_account_postgres,
test_get_or_create_user_by_github_account_sqlite
);
async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) {
async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
db.create_user(
"user1@example.com",
None,
@@ -104,14 +104,7 @@ async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) {
.user_id;
let user = db
.update_or_create_user_by_github_account(
"the-new-login2",
102,
None,
None,
Utc::now(),
None,
)
.get_or_create_user_by_github_account("the-new-login2", 102, None, None, Utc::now(), None)
.await
.unwrap();
assert_eq!(user.id, user_id2);
@@ -119,7 +112,7 @@ async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) {
assert_eq!(user.github_user_id, 102);
let user = db
.update_or_create_user_by_github_account(
.get_or_create_user_by_github_account(
"login3",
103,
Some("user3@example.com"),

View File

@@ -14,7 +14,7 @@ use crate::{
db::{
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult,
NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId,
RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,
},
executor::Executor,
@@ -1890,16 +1890,28 @@ async fn join_project(
let db = session.db().await;
let (project, replica_id) = &mut *db
.join_project(
project_id,
session.connection_id,
session.user_id(),
request.committer_name.clone(),
request.committer_email.clone(),
)
.join_project(project_id, session.connection_id, session.user_id())
.await?;
drop(db);
tracing::info!(%project_id, "join remote project");
join_project_internal(response, session, project, replica_id)
}
trait JoinProjectInternalResponse {
fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
}
impl JoinProjectInternalResponse for Response<proto::JoinProject> {
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
Response::<proto::JoinProject>::send(self, result)
}
}
fn join_project_internal(
response: impl JoinProjectInternalResponse,
session: Session,
project: &mut Project,
replica_id: &ReplicaId,
) -> Result<()> {
let collaborators = project
.collaborators
.iter()
@@ -1927,8 +1939,6 @@ async fn join_project(
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
is_host: false,
committer_name: request.committer_name.clone(),
committer_email: request.committer_email.clone(),
}),
};
@@ -2557,6 +2567,7 @@ async fn get_users(
id: user.id.to_proto(),
avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
github_login: user.github_login,
email: user.email_address,
name: user.name,
})
.collect();
@@ -2590,6 +2601,7 @@ async fn fuzzy_search_users(
avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
github_login: user.github_login,
name: user.name,
email: user.email_address,
})
.collect();
response.send(proto::UsersResponse { users })?;

View File

@@ -127,7 +127,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
log::info!("Seeding {:?} from GitHub", github_user.login);
let user = db
.update_or_create_user_by_github_account(
.get_or_create_user_by_github_account(
&github_user.login,
github_user.id,
github_user.email.as_deref(),

View File

@@ -180,7 +180,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
server
.app_state
.db
.update_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None)
.get_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None)
.await
.unwrap();

View File

@@ -1610,8 +1610,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
.root(cx_a)
.unwrap();
executor.run_until_parked();
workspace_a_project_b.update(cx_a2, |workspace, cx| {
assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));

View File

@@ -51,7 +51,7 @@ use std::{
time::Duration,
};
use unindent::Unindent as _;
use util::{path, uri};
use util::{path, separator, uri};
use workspace::Pane;
#[ctor::ctor]
@@ -1676,13 +1676,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
path!("a.txt"),
path!("b.txt"),
path!("subdir2"),
path!("subdir2/f.txt"),
path!("subdir2/g.txt"),
path!("subdir2/h.txt"),
path!("subdir2/i.txt")
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/i.txt")
]
);
assert!(worktree_a3.read(cx).has_update_observer());
@@ -1709,13 +1709,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
path!("a.txt"),
path!("b.txt"),
path!("subdir2"),
path!("subdir2/f.txt"),
path!("subdir2/g.txt"),
path!("subdir2/h.txt"),
path!("subdir2/i.txt")
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/i.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1806,13 +1806,13 @@ async fn test_project_reconnect(
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
vec![
path!("a.txt"),
path!("b.txt"),
path!("subdir2"),
path!("subdir2/f.txt"),
path!("subdir2/g.txt"),
path!("subdir2/h.txt"),
path!("subdir2/j.txt")
separator!("a.txt"),
separator!("b.txt"),
separator!("subdir2"),
separator!("subdir2/f.txt"),
separator!("subdir2/g.txt"),
separator!("subdir2/h.txt"),
separator!("subdir2/j.txt")
]
);
assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1876,6 +1876,7 @@ async fn test_active_call_events(
github_login: "user_a".to_string(),
avatar_uri: "avatar_a".into(),
name: None,
email: None,
}),
project_id: project_a_id,
worktree_root_names: vec!["a".to_string()],
@@ -1895,6 +1896,7 @@ async fn test_active_call_events(
github_login: "user_b".to_string(),
avatar_uri: "avatar_b".into(),
name: None,
email: None,
}),
project_id: project_b_id,
worktree_root_names: vec!["b".to_string()]
@@ -3315,13 +3317,13 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
path!("DIR"),
path!("DIR/SUBDIR"),
path!("DIR/SUBDIR/f.txt"),
path!("DIR/e.txt"),
path!("a.txt"),
path!("b.txt"),
path!("d.txt")
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt")
]
);
});
@@ -3333,13 +3335,13 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
path!("DIR"),
path!("DIR/SUBDIR"),
path!("DIR/SUBDIR/f.txt"),
path!("DIR/e.txt"),
path!("a.txt"),
path!("b.txt"),
path!("d.txt")
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt")
]
);
});
@@ -3359,14 +3361,14 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
path!("DIR"),
path!("DIR/SUBDIR"),
path!("DIR/SUBDIR/f.txt"),
path!("DIR/e.txt"),
path!("a.txt"),
path!("b.txt"),
path!("d.txt"),
path!("f.txt")
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt"),
separator!("f.txt")
]
);
});
@@ -3378,14 +3380,14 @@ async fn test_fs_operations(
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[
path!("DIR"),
path!("DIR/SUBDIR"),
path!("DIR/SUBDIR/f.txt"),
path!("DIR/e.txt"),
path!("a.txt"),
path!("b.txt"),
path!("d.txt"),
path!("f.txt")
separator!("DIR"),
separator!("DIR/SUBDIR"),
separator!("DIR/SUBDIR/f.txt"),
separator!("DIR/e.txt"),
separator!("a.txt"),
separator!("b.txt"),
separator!("d.txt"),
separator!("f.txt")
]
);
});

View File

@@ -30,7 +30,7 @@ use rpc::proto;
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
use util::path;
use util::{path, separator};
#[gpui::test(iterations = 10)]
async fn test_sharing_an_ssh_remote_project(
@@ -198,7 +198,7 @@ async fn test_sharing_an_ssh_remote_project(
.path()
.to_string_lossy()
.to_string(),
path!("src/renamed.rs").to_string()
separator!("src/renamed.rs").to_string()
);
});
}
@@ -671,7 +671,7 @@ async fn test_remote_server_debugger(
});
session.update(cx_a, |session, _| {
assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
assert_eq!(session.binary().command, "ssh");
});
let shutdown_session = workspace.update(cx_a, |workspace, cx| {

View File

@@ -82,7 +82,7 @@ impl UserBackfiller {
{
Ok(github_user) => {
self.db
.update_or_create_user_by_github_account(
.get_or_create_user_by_github_account(
&user.github_login,
github_user.id,
user.email_address.as_deref(),

View File

@@ -90,7 +90,7 @@ impl ChatPanel {
languages.clone(),
user_store.clone(),
None,
cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
cx.new(|cx| Editor::auto_height(4, window, cx)),
window,
cx,
)
@@ -1218,6 +1218,7 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
email: None,
}),
nonce: 5,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
@@ -1273,6 +1274,7 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
email: None,
}),
nonce: 5,
mentions: Vec::new(),
@@ -1321,6 +1323,7 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
email: None,
}),
nonce: 5,
mentions: Vec::new(),

View File

@@ -26,7 +26,6 @@ use parking_lot::Mutex;
use request::StatusNotification;
use settings::SettingsStore;
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
use std::collections::hash_map::Entry;
use std::{
any::TypeId,
env,
@@ -732,43 +731,42 @@ impl Copilot {
return;
}
let entry = registered_buffers.entry(buffer.entity_id());
if let Entry::Vacant(e) = entry {
let Ok(uri) = uri_for_buffer(buffer, cx) else {
return;
};
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
server
.notify::<lsp::notification::DidOpenTextDocument>(
&lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: uri.clone(),
language_id: language_id.clone(),
version: 0,
text: snapshot.text(),
registered_buffers
.entry(buffer.entity_id())
.or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
server
.notify::<lsp::notification::DidOpenTextDocument>(
&lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: uri.clone(),
language_id: language_id.clone(),
version: 0,
text: snapshot.text(),
},
},
},
)
.ok();
)
.ok();
e.insert(RegisteredBuffer {
uri,
language_id,
snapshot,
snapshot_version: 0,
pending_buffer_change: Task::ready(Some(())),
_subscriptions: [
cx.subscribe(buffer, |this, buffer, event, cx| {
this.handle_buffer_event(buffer, event, cx).log_err();
}),
cx.observe_release(buffer, move |this, _buffer, _cx| {
this.buffers.remove(&weak_buffer);
this.unregister_buffer(&weak_buffer);
}),
],
RegisteredBuffer {
uri,
language_id,
snapshot,
snapshot_version: 0,
pending_buffer_change: Task::ready(Some(())),
_subscriptions: [
cx.subscribe(buffer, |this, buffer, event, cx| {
this.handle_buffer_event(buffer, event, cx).log_err();
}),
cx.observe_release(buffer, move |this, _buffer, _cx| {
this.buffers.remove(&weak_buffer);
this.unregister_buffer(&weak_buffer);
}),
],
}
});
}
}
}
@@ -800,9 +798,7 @@ impl Copilot {
language::BufferEvent::FileHandleChanged
| language::BufferEvent::LanguageChanged => {
let new_language_id = id_for_language(buffer.read(cx).language());
let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
return Ok(());
};
let new_uri = uri_for_buffer(&buffer, cx);
if new_uri != registered_buffer.uri
|| new_language_id != registered_buffer.language_id
{
@@ -1072,13 +1068,11 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
.unwrap_or_else(|| "plaintext".to_string())
}
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> lsp::Url {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx))
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
} else {
format!("buffer://{}", buffer.entity_id())
.parse()
.map_err(|_| ())
format!("buffer://{}", buffer.entity_id()).parse().unwrap()
}
}

View File

@@ -51,9 +51,6 @@ telemetry.workspace = true
util.workspace = true
workspace-hack.workspace = true
[target.'cfg(not(windows))'.dependencies]
libc.workspace = true
[dev-dependencies]
async-pipe.workspace = true
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -93,7 +93,7 @@ impl<'a> From<&'a str> for DebugAdapterName {
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[derive(Debug, Clone, PartialEq)]
pub struct TcpArguments {
pub host: Ipv4Addr,
pub port: u16,
@@ -179,9 +179,9 @@ impl DebugTaskDefinition {
}
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[derive(Debug, Clone, PartialEq)]
pub struct DebugAdapterBinary {
pub command: Option<String>,
pub command: String,
pub arguments: Vec<String>,
pub envs: HashMap<String, String>,
pub cwd: Option<PathBuf>,
@@ -368,11 +368,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
}
}
fn dap_schema(&self) -> serde_json::Value;
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None
}
async fn dap_schema(&self) -> serde_json::Value;
}
#[cfg(any(test, feature = "test-support"))]
@@ -394,7 +390,7 @@ impl DebugAdapter for FakeAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
@@ -437,7 +433,7 @@ impl DebugAdapter for FakeAdapter {
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
Ok(DebugAdapterBinary {
command: Some("command".into()),
command: "command".into(),
arguments: vec![],
connection: None,
envs: HashMap::default(),

View File

@@ -152,7 +152,8 @@ impl DebugAdapterClient {
arguments: Some(serialized_arguments),
};
self.transport_delegate
.add_pending_request(sequence_id, callback_tx);
.add_pending_request(sequence_id, callback_tx)
.await;
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
@@ -217,7 +218,7 @@ impl DebugAdapterClient {
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
where
F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
F: 'static + Send + FnMut(IoKind, &str),
{
self.transport_delegate.add_log_handler(f, kind);
}
@@ -296,7 +297,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),
connection: None,
@@ -366,7 +367,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),
connection: None,
@@ -419,7 +420,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
command: "command".into(),
arguments: Default::default(),
envs: Default::default(),
connection: None,

View File

@@ -14,16 +14,16 @@ use crate::{
};
use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
#[async_trait]
pub trait DapLocator: Send + Sync {
fn name(&self) -> SharedString;
/// Determines whether this locator can generate debug target for given task.
async fn create_scenario(
fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: &DebugAdapterName,
adapter: DebugAdapterName,
) -> Option<DebugScenario>;
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
@@ -67,12 +67,13 @@ impl DapRegistry {
pub async fn adapters_schema(&self) -> task::AdapterSchemas {
let mut schemas = AdapterSchemas(vec![]);
// Clone to avoid holding lock over await points
let adapters = self.0.read().adapters.clone();
for (name, adapter) in adapters.into_iter() {
schemas.0.push(AdapterSchema {
adapter: name.into(),
schema: adapter.dap_schema(),
schema: adapter.dap_schema().await,
});
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, bail};
use dap_types::{
ErrorResponse,
messages::{Message, Response},
@@ -12,6 +12,7 @@ use smol::{
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
lock::Mutex,
net::{TcpListener, TcpStream},
process::Child,
};
use std::{
collections::HashMap,
@@ -21,13 +22,11 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::ConnectionResult;
use util::{ConnectionResult, ResultExt as _};
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
pub(crate) type IoMessage = str;
pub(crate) type Command = str;
pub type IoHandler = Box<dyn Send + FnMut(IoKind, Option<&Command>, &IoMessage)>;
pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LogKind {
@@ -64,7 +63,7 @@ impl TransportPipe {
}
}
type Requests = Arc<parking_lot::Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
type LogHandlers = Arc<parking_lot::Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
pub enum Transport {
@@ -87,12 +86,10 @@ impl Transport {
TcpTransport::start(binary, cx)
.await
.map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
.context("Tried to connect to a debug adapter via TCP transport layer")
} else {
StdioTransport::start(binary, cx)
.await
.map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
.context("Tried to connect to a debug adapter via stdin/stdout transport layer")
}
}
@@ -105,7 +102,7 @@ impl Transport {
}
}
async fn kill(&self) {
async fn kill(&self) -> Result<()> {
match self {
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
@@ -125,6 +122,7 @@ impl Transport {
pub(crate) struct TransportDelegate {
log_handlers: LogHandlers,
current_requests: Requests,
pending_requests: Requests,
transport: Transport,
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
@@ -141,6 +139,7 @@ impl TransportDelegate {
transport,
server_tx: Default::default(),
log_handlers: Default::default(),
current_requests: Default::default(),
pending_requests: Default::default(),
_tasks: Vec::new(),
};
@@ -192,7 +191,7 @@ impl TransportDelegate {
match Self::handle_output(
params.output,
client_tx,
pending_requests.clone(),
pending_requests,
output_log_handler,
)
.await
@@ -200,12 +199,6 @@ impl TransportDelegate {
Ok(()) => {}
Err(e) => log::error!("Error handling debugger output: {e}"),
}
let mut pending_requests = pending_requests.lock();
pending_requests.drain().for_each(|(_, request)| {
request
.send(Err(anyhow!("debugger shutdown unexpectedly")))
.ok();
});
}));
if let Some(stderr) = params.stderr.take() {
@@ -226,9 +219,19 @@ impl TransportDelegate {
}));
}
let current_requests = self.current_requests.clone();
let pending_requests = self.pending_requests.clone();
let log_handler = log_handler.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_input(params.input, client_rx, log_handler).await {
match Self::handle_input(
params.input,
client_rx,
current_requests,
pending_requests,
log_handler,
)
.await
{
Ok(()) => {}
Err(e) => log::error!("Error handling debugger input: {e}"),
}
@@ -243,12 +246,12 @@ impl TransportDelegate {
Ok((server_rx, server_tx))
}
pub(crate) fn add_pending_request(
pub(crate) async fn add_pending_request(
&self,
sequence_id: u64,
request: oneshot::Sender<Result<Response>>,
) {
let mut pending_requests = self.pending_requests.lock();
let mut pending_requests = self.pending_requests.lock().await;
pending_requests.insert(sequence_id, request);
}
@@ -286,7 +289,7 @@ impl TransportDelegate {
if let Some(log_handlers) = log_handlers.as_ref() {
for (kind, handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Adapter) {
handler(IoKind::StdOut, None, line.as_str());
handler(IoKind::StdOut, line.as_str());
}
}
}
@@ -304,6 +307,8 @@ impl TransportDelegate {
async fn handle_input<Stdin>(
mut server_stdin: Stdin,
client_rx: Receiver<Message>,
current_requests: Requests,
pending_requests: Requests,
log_handlers: Option<LogHandlers>,
) -> Result<()>
where
@@ -312,11 +317,11 @@ impl TransportDelegate {
let result = loop {
match client_rx.recv().await {
Ok(message) => {
let command = match &message {
Message::Request(request) => Some(request.command.as_str()),
Message::Response(response) => Some(response.command.as_str()),
_ => None,
};
if let Message::Request(request) = &message {
if let Some(sender) = current_requests.lock().await.remove(&request.seq) {
pending_requests.lock().await.insert(request.seq, sender);
}
}
let message = match serde_json::to_string(&message) {
Ok(message) => message,
@@ -326,7 +331,7 @@ impl TransportDelegate {
if let Some(log_handlers) = log_handlers.as_ref() {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Rpc) {
log_handler(IoKind::StdIn, command, &message);
log_handler(IoKind::StdIn, &message);
}
}
}
@@ -373,8 +378,7 @@ impl TransportDelegate {
return Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
let tx = pending_requests.lock().remove(&res.request_seq);
if let Some(tx) = tx {
if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);
}
@@ -412,7 +416,7 @@ impl TransportDelegate {
Ok(_) => {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Adapter) {
log_handler(IoKind::StdErr, None, buffer.as_str());
log_handler(IoKind::StdErr, buffer.as_str());
}
}
@@ -501,24 +505,17 @@ impl TransportDelegate {
Err(e) => return ConnectionResult::Result(Err(e)),
};
let message =
serde_json::from_str::<Message>(message_str).context("deserializing server message");
if let Some(log_handlers) = log_handlers {
let command = match &message {
Ok(Message::Request(request)) => Some(request.command.as_str()),
Ok(Message::Response(response)) => Some(response.command.as_str()),
_ => None,
};
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Rpc) {
log_handler(IoKind::StdOut, command, message_str);
log_handler(IoKind::StdOut, message_str);
}
}
}
ConnectionResult::Result(message)
ConnectionResult::Result(
serde_json::from_str::<Message>(message_str).context("deserializing server message"),
)
}
pub async fn shutdown(&self) -> Result<()> {
@@ -528,8 +525,16 @@ impl TransportDelegate {
server_tx.close();
}
self.pending_requests.lock().clear();
self.transport.kill().await;
let mut current_requests = self.current_requests.lock().await;
let mut pending_requests = self.pending_requests.lock().await;
current_requests.clear();
pending_requests.clear();
let _ = self.transport.kill().await.log_err();
drop(current_requests);
drop(pending_requests);
log::debug!("Shutdown client completed");
@@ -546,7 +551,7 @@ impl TransportDelegate {
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
where
F: 'static + Send + FnMut(IoKind, Option<&Command>, &IoMessage),
F: 'static + Send + FnMut(IoKind, &str),
{
let mut log_handlers = self.log_handlers.lock();
log_handlers.push((kind, Box::new(f)));
@@ -557,7 +562,7 @@ pub struct TcpTransport {
pub port: u16,
pub host: Ipv4Addr,
pub timeout: u64,
process: Option<Mutex<Child>>,
process: Mutex<Child>,
}
impl TcpTransport {
@@ -586,23 +591,26 @@ impl TcpTransport {
let host = connection_args.host;
let port = connection_args.port;
let mut process = if let Some(command) = &binary.command {
let mut command = util::command::new_std_command(&command);
let mut command = util::command::new_std_command(&binary.command);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
}
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
}
command.args(&binary.arguments);
command.envs(&binary.envs);
command.args(&binary.arguments);
command.envs(&binary.envs);
Some(
Child::spawn(command, Stdio::null())
.with_context(|| "failed to start debug adapter.")?,
)
} else {
None
};
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command
.spawn()
.with_context(|| "failed to start debug adapter.")?;
let address = SocketAddrV4::new(host, port);
@@ -620,18 +628,15 @@ impl TcpTransport {
match TcpStream::connect(address).await {
Ok(stream) => return Ok((process, stream.split())),
Err(_) => {
if let Some(p) = &mut process {
if let Ok(Some(_)) = p.try_status() {
let output = process.take().unwrap().into_inner().output().await?;
let output = if output.stderr.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
};
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
}
if let Ok(Some(_)) = process.try_status() {
let output = process.output().await?;
let output = if output.stderr.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
};
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
}
cx.background_executor().timer(Duration::from_millis(100)).await;
}
}
@@ -644,13 +649,13 @@ impl TcpTransport {
host,
port
);
let stdout = process.as_mut().and_then(|p| p.stdout.take());
let stderr = process.as_mut().and_then(|p| p.stderr.take());
let stdout = process.stdout.take();
let stderr = process.stderr.take();
let this = Self {
port,
host,
process: process.map(Mutex::new),
process: Mutex::new(process),
timeout,
};
@@ -668,19 +673,10 @@ impl TcpTransport {
true
}
async fn kill(&self) {
if let Some(process) = &self.process {
let mut process = process.lock().await;
Child::kill(&mut process);
}
}
}
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
impl Drop for TcpTransport {
fn drop(&mut self) {
if let Some(mut p) = self.process.take() {
p.get_mut().kill();
}
Ok(())
}
}
@@ -691,12 +687,9 @@ pub struct StdioTransport {
impl StdioTransport {
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
let Some(binary_command) = &binary.command else {
bail!(
"When using the `stdio` transport, the path to a debug adapter binary must be set by Zed."
);
};
let mut command = util::command::new_std_command(&binary_command);
let mut command = util::command::new_std_command(&binary.command);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
@@ -705,10 +698,16 @@ impl StdioTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command.spawn().with_context(|| {
format!(
"failed to spawn command `{} {}`.",
binary_command,
binary.command,
binary.arguments.join(" ")
)
})?;
@@ -723,7 +722,7 @@ impl StdioTransport {
if stderr.is_none() {
bail!(
"Failed to connect to stderr for debug adapter command {}",
&binary_command
&binary.command
);
}
@@ -746,15 +745,9 @@ impl StdioTransport {
false
}
async fn kill(&self) {
let mut process = self.process.lock().await;
Child::kill(&mut process);
}
}
impl Drop for StdioTransport {
fn drop(&mut self) {
self.process.get_mut().kill();
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
Ok(())
}
}
@@ -928,66 +921,7 @@ impl FakeTransport {
false
}
async fn kill(&self) {}
}
struct Child {
process: smol::process::Child,
}
impl std::ops::Deref for Child {
type Target = smol::process::Child;
fn deref(&self) -> &Self::Target {
&self.process
}
}
impl std::ops::DerefMut for Child {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.process
}
}
impl Child {
fn into_inner(self) -> smol::process::Child {
self.process
}
#[cfg(not(windows))]
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
util::set_pre_exec_to_start_new_session(&mut command);
let process = smol::process::Command::from(command)
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(Self { process })
}
#[cfg(windows)]
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let process = smol::process::Command::from(command)
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(Self { process })
}
#[cfg(not(windows))]
fn kill(&mut self) {
let pid = self.process.id();
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
fn kill(&mut self) {
// TODO(windows): terminate the job object in kill
let _ = self.process.kill();
async fn kill(&self) -> Result<()> {
Ok(())
}
}

View File

@@ -21,21 +21,18 @@ impl CodeLldbDebugAdapter {
fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
// CodeLLDB uses `name` for a terminal label.
let mut configuration = task_definition.config.clone();
let obj = configuration
configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?;
obj.entry("name")
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
.context("CodeLLDB is not a valid json object")?
.insert(
"name".into(),
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.request_kind(&configuration)?;
@@ -133,7 +130,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
})
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
@@ -362,13 +359,13 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
Ok(DebugAdapterBinary {
command: Some(command.unwrap()),
command: command.unwrap(),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
],
request_args: self.request_args(delegate, &config)?,
request_args: self.request_args(&config)?,
envs: HashMap::default(),
connection: None,
})

View File

@@ -63,7 +63,7 @@ impl DebugAdapter for GdbDebugAdapter {
})
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
@@ -177,23 +177,18 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let mut configuration = config.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
let request_args = StartDebuggingRequestArguments {
request: self.request_kind(&config.config)?,
configuration: config.config.clone(),
};
Ok(DebugAdapterBinary {
command: Some(gdb_path),
command: gdb_path,
arguments: vec!["-i=dap".into()],
envs: HashMap::default(),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
connection: None,
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&config.config)?,
configuration,
},
request_args,
})
}
}

View File

@@ -1,17 +1,15 @@
use anyhow::{Context as _, bail};
use collections::HashMap;
use dap::{
StartDebuggingRequestArguments,
adapters::{
DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
latest_github_release,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
use task::TcpArgumentsTemplate;
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util;
use crate::*;
@@ -96,7 +94,7 @@ impl DebugAdapter for GoDebugAdapter {
Some(SharedString::new_static("Go").into())
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
// Create common properties shared between launch and attach
let common_properties = json!({
"debugAdapter": {
@@ -435,6 +433,10 @@ impl DebugAdapter for GoDebugAdapter {
adapter_path.join("dlv").to_string_lossy().to_string()
};
let minidelve_path = self.install_shim(delegate).await?;
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
let cwd = task_definition
.config
@@ -443,58 +445,31 @@ impl DebugAdapter for GoDebugAdapter {
.map(PathBuf::from)
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
let arguments;
let command;
let connection;
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
if let Some(connection_options) = &task_definition.tcp_connection {
command = None;
arguments = vec![];
let (host, port, timeout) =
crate::configure_tcp_connection(connection_options.clone()).await?;
connection = Some(TcpArguments {
host,
port,
timeout,
});
let arguments = if cfg!(windows) {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
"--headless".into(),
]
} else {
let minidelve_path = self.install_shim(delegate).await?;
let (host, port, _) =
crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
command = Some(minidelve_path.to_string_lossy().into_owned());
connection = None;
arguments = if cfg!(windows) {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
"--headless".into(),
]
} else {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
]
};
}
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
]
};
Ok(DebugAdapterBinary {
command,
command: minidelve_path.to_string_lossy().into_owned(),
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
connection,
connection: None,
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: self.request_kind(&task_definition.config)?,
},
})

View File

@@ -2,7 +2,6 @@ use adapters::latest_github_release;
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::ResultExt;
@@ -69,44 +68,13 @@ 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 configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
if let Some(program) = configuration
.get("program")
.cloned()
.and_then(|value| value.as_str().map(str::to_owned))
{
match program.as_str() {
"npm" | "pnpm" | "yarn" | "bun"
if !configuration.contains_key("runtimeExecutable")
&& !configuration.contains_key("runtimeArgs") =>
{
configuration.remove("program");
configuration.insert("runtimeExecutable".to_owned(), program.into());
if let Some(args) = configuration.remove("args") {
configuration.insert("runtimeArgs".to_owned(), args);
}
}
_ => {}
}
}
configuration
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
configuration.entry("type").and_modify(normalize_task_type);
}
Ok(DebugAdapterBinary {
command: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -123,7 +91,7 @@ impl JsDebugAdapter {
timeout,
}),
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: self.request_kind(&task_definition.config)?,
},
})
@@ -182,7 +150,7 @@ impl DebugAdapter for JsDebugAdapter {
})
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
@@ -203,7 +171,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", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
@@ -463,25 +431,4 @@ impl DebugAdapter for JsDebugAdapter {
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
let label = args.configuration.get("name")?.as_str()?;
Some(label.to_owned())
}
}
fn normalize_task_type(task_type: &mut Value) {
let Some(task_type_str) = task_type.as_str() else {
return;
};
let new_name = match task_type_str {
"node" | "pwa-node" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,
}
.to_owned();
*task_type = Value::String(new_name);
}

View File

@@ -71,21 +71,13 @@ impl PhpDebugAdapter {
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());
}
Ok(DebugAdapterBinary {
command: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -101,7 +93,7 @@ impl PhpDebugAdapter {
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})
@@ -110,7 +102,7 @@ impl PhpDebugAdapter {
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {

View File

@@ -83,7 +83,6 @@ impl PythonDebugAdapter {
fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.request_kind(&task_definition.config)?;
@@ -96,11 +95,6 @@ impl PythonDebugAdapter {
}
}
if let Some(obj) = configuration.as_object_mut() {
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
}
Ok(StartDebuggingRequestArguments {
configuration,
request,
@@ -193,7 +187,7 @@ impl PythonDebugAdapter {
);
Ok(DebugAdapterBinary {
command: Some(python_command),
command: python_command,
arguments,
connection: Some(adapters::TcpArguments {
host,
@@ -202,7 +196,7 @@ impl PythonDebugAdapter {
}),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(delegate, config)?,
request_args: self.request_args(config)?,
})
}
}
@@ -257,7 +251,7 @@ impl DebugAdapter for PythonDebugAdapter {
})
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {

View File

@@ -49,7 +49,7 @@ impl DebugAdapter for RubyDebugAdapter {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
fn dap_schema(&self) -> serde_json::Value {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
@@ -174,15 +174,8 @@ impl DebugAdapter for RubyDebugAdapter {
arguments.extend(ruby_config.args);
let mut configuration = definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: Some(rdbg_path.to_string_lossy().to_string()),
command: rdbg_path.to_string_lossy().to_string(),
arguments,
connection: Some(dap::adapters::TcpArguments {
host,
@@ -197,7 +190,7 @@ impl DebugAdapter for RubyDebugAdapter {
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config)?,
configuration,
configuration: definition.config.clone(),
},
})
}

View File

@@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
}
async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
log::info!("Opening database {}", db_path.display());
log::info!("Opening main db");
ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
@@ -84,7 +84,7 @@ async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnectio
}
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
log::warn!("Opening fallback in-memory database");
log::info!("Opening fallback db");
ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)

View File

@@ -12,7 +12,6 @@ dap.workspace = true
extension.workspace = true
gpui.workspace = true
serde_json.workspace = true
util.workspace = true
task.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }

View File

@@ -1,5 +1,4 @@
mod extension_dap_adapter;
mod extension_locator_adapter;
use std::sync::Arc;
@@ -7,9 +6,6 @@ use dap::DapRegistry;
use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy};
use extension_dap_adapter::ExtensionDapAdapter;
use gpui::App;
use util::ResultExt;
use crate::extension_locator_adapter::ExtensionLocatorAdapter;
pub fn init(extension_host_proxy: Arc<ExtensionHostProxy>, cx: &mut App) {
let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx);
@@ -34,21 +30,11 @@ impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy {
&self,
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) {
if let Some(adapter) = ExtensionDapAdapter::new(extension, debug_adapter_name).log_err() {
self.debug_adapter_registry.add_adapter(Arc::new(adapter));
}
}
fn register_debug_locator(
&self,
extension: Arc<dyn extension::Extension>,
locator_name: Arc<str>,
) {
self.debug_adapter_registry
.add_locator(Arc::new(ExtensionLocatorAdapter::new(
.add_adapter(Arc::new(ExtensionDapAdapter::new(
extension,
locator_name,
debug_adapter_name,
)));
}
}

View File

@@ -1,10 +1,6 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use std::{path::PathBuf, sync::Arc};
use anyhow::{Context, Result};
use anyhow::Result;
use async_trait::async_trait;
use dap::adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
@@ -16,26 +12,17 @@ use task::{DebugScenario, ZedDebugConfig};
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
debug_adapter_name: Arc<str>,
schema: serde_json::Value,
}
impl ExtensionDapAdapter {
pub(crate) fn new(
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) -> Result<Self> {
let schema = std::fs::read_to_string(extension.path_from_extension(
&Path::new("debug_adapter_schemas").join(debug_adapter_name.as_ref()),
))
.with_context(|| format!("Failed to read debug adapter schema for {debug_adapter_name}"))?;
let schema = serde_json::Value::from_str(&schema).with_context(|| {
format!("Debug adapter schema for {debug_adapter_name} is not a valid JSON")
})?;
Ok(Self {
) -> Self {
Self {
extension,
debug_adapter_name,
schema,
})
}
}
}
@@ -74,8 +61,8 @@ impl DebugAdapter for ExtensionDapAdapter {
self.debug_adapter_name.as_ref().into()
}
fn dap_schema(&self) -> serde_json::Value {
self.schema.clone()
async fn dap_schema(&self) -> serde_json::Value {
self.extension.get_dap_schema().await.unwrap_or_default()
}
async fn get_binary(

View File

@@ -1,50 +0,0 @@
use anyhow::Result;
use async_trait::async_trait;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use extension::Extension;
use gpui::SharedString;
use std::sync::Arc;
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
pub(crate) struct ExtensionLocatorAdapter {
extension: Arc<dyn Extension>,
locator_name: SharedString,
}
impl ExtensionLocatorAdapter {
pub(crate) fn new(extension: Arc<dyn extension::Extension>, locator_name: Arc<str>) -> Self {
Self {
extension,
locator_name: SharedString::from(locator_name),
}
}
}
#[async_trait]
impl DapLocator for ExtensionLocatorAdapter {
fn name(&self) -> SharedString {
self.locator_name.clone()
}
/// Determines whether this locator can generate debug target for given task.
async fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: &DebugAdapterName,
) -> Option<DebugScenario> {
self.extension
.dap_locator_create_scenario(
self.locator_name.as_ref().to_owned(),
build_config.clone(),
resolved_label.to_owned(),
adapter.0.as_ref().to_owned(),
)
.await
.ok()
.flatten()
}
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
Err(anyhow::anyhow!("Not implemented"))
}
}

View File

@@ -1,5 +1,4 @@
use dap::{
adapters::DebugAdapterName,
client::SessionId,
debugger_settings::DebuggerSettings,
transport::{IoKind, LogKind},
@@ -32,13 +31,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
// TODO:
// - [x] stop sorting by session ID
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
// - [ ] dump the launch/attach request somewhere (logs?)
const MAX_SESSIONS: usize = 10;
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
@@ -51,9 +43,9 @@ struct DapLogView {
pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_sessions: VecDeque<DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
debug_clients: HashMap<SessionId, DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
}
struct ProjectState {
@@ -61,19 +53,13 @@ struct ProjectState {
}
struct DebugAdapterState {
id: SessionId,
log_messages: VecDeque<SharedString>,
log_messages: VecDeque<String>,
rpc_messages: RpcMessages,
adapter_name: DebugAdapterName,
has_adapter_logs: bool,
is_terminated: bool,
}
struct RpcMessages {
messages: VecDeque<SharedString>,
messages: VecDeque<String>,
last_message_kind: Option<MessageKind>,
initialization_sequence: Vec<SharedString>,
last_init_message_kind: Option<MessageKind>,
}
impl RpcMessages {
@@ -82,9 +68,7 @@ impl RpcMessages {
fn new() -> Self {
Self {
last_message_kind: None,
last_init_message_kind: None,
messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
initialization_sequence: Vec::new(),
}
}
}
@@ -108,27 +92,22 @@ impl MessageKind {
}
impl DebugAdapterState {
fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
fn new() -> Self {
Self {
id,
log_messages: VecDeque::new(),
rpc_messages: RpcMessages::new(),
adapter_name,
has_adapter_logs,
is_terminated: false,
}
}
}
impl LogStore {
pub fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
this.on_rpc_log(client_id, io_kind, &message, cx);
})?;
}
@@ -138,13 +117,12 @@ impl LogStore {
})
.detach_and_log_err(cx);
let (adapter_log_tx, mut adapter_log_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_log(session_id, io_kind, message, cx);
this.on_adapter_log(client_id, io_kind, &message, cx);
})?;
}
@@ -157,10 +135,30 @@ impl LogStore {
rpc_tx,
adapter_log_tx,
projects: HashMap::new(),
debug_sessions: Default::default(),
debug_clients: HashMap::new(),
}
}
fn on_rpc_log(
&mut self,
client_id: SessionId,
io_kind: IoKind,
message: &str,
cx: &mut Context<Self>,
) {
self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
}
fn on_adapter_log(
&mut self,
client_id: SessionId,
io_kind: IoKind,
message: &str,
cx: &mut Context<Self>,
) {
self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
}
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
let weak_project = project.downgrade();
self.projects.insert(
@@ -176,15 +174,13 @@ impl LogStore {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session {
this.add_debug_session(*session_id, session, cx);
this.add_debug_client(*session_id, session, cx);
}
}
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
this.get_debug_adapter_state(*session_id)
.iter_mut()
.for_each(|state| state.is_terminated = true);
this.clean_sessions(cx);
this.remove_debug_client(*session_id, cx);
}
_ => {}
},
),
@@ -194,88 +190,63 @@ impl LogStore {
}
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
self.debug_sessions
.iter_mut()
.find(|adapter_state| adapter_state.id == id)
self.debug_clients.get_mut(&id)
}
fn add_debug_adapter_message(
fn add_debug_client_message(
&mut self,
id: SessionId,
io_kind: IoKind,
command: Option<SharedString>,
message: SharedString,
message: String,
cx: &mut Context<Self>,
) {
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
return;
};
let is_init_seq = command.as_ref().is_some_and(|command| {
matches!(
command.as_ref(),
"attach" | "launch" | "initialize" | "configurationDone"
)
});
let kind = match io_kind {
IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
IoKind::StdIn => MessageKind::Send,
};
let rpc_messages = &mut debug_client_state.rpc_messages;
// Push a separator if the kind has changed
if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry(
Self::add_debug_client_entry(
&mut rpc_messages.messages,
id,
kind.label().into(),
kind.label().to_string(),
LogKind::Rpc,
cx,
);
rpc_messages.last_message_kind = Some(kind);
}
let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
id,
message,
LogKind::Rpc,
cx,
);
if is_init_seq {
if rpc_messages.last_init_message_kind != Some(kind) {
rpc_messages
.initialization_sequence
.push(SharedString::from(kind.label()));
rpc_messages.last_init_message_kind = Some(kind);
}
rpc_messages.initialization_sequence.push(entry);
}
Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
cx.notify();
}
fn add_debug_adapter_log(
fn add_debug_client_log(
&mut self,
id: SessionId,
io_kind: IoKind,
message: SharedString,
message: String,
cx: &mut Context<Self>,
) {
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
return;
};
let message = match io_kind {
IoKind::StdErr => format!("stderr: {message}").into(),
IoKind::StdErr => {
let mut message = message.clone();
message.insert_str(0, "stderr: ");
message
}
_ => message,
};
Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages,
Self::add_debug_client_entry(
&mut debug_client_state.log_messages,
id,
message,
LogKind::Adapter,
@@ -284,13 +255,13 @@ impl LogStore {
cx.notify();
}
fn get_debug_adapter_entry(
log_lines: &mut VecDeque<SharedString>,
fn add_debug_client_entry(
log_lines: &mut VecDeque<String>,
id: SessionId,
message: SharedString,
message: String,
kind: LogKind,
cx: &mut Context<Self>,
) -> SharedString {
) {
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
log_lines.pop_front();
}
@@ -304,69 +275,33 @@ impl LogStore {
)
.ok()
})
.map(SharedString::from)
.unwrap_or(message)
} else {
message
};
log_lines.push_back(entry.clone());
cx.emit(Event::NewLogEntry {
id,
entry: entry.clone(),
kind,
});
entry
cx.emit(Event::NewLogEntry { id, entry, kind });
}
fn add_debug_session(
fn add_debug_client(
&mut self,
session_id: SessionId,
session: Entity<Session>,
cx: &mut Context<Self>,
) {
if self
.debug_sessions
.iter_mut()
.any(|adapter_state| adapter_state.id == session_id)
{
return;
}
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map(|client| client.has_adapter_logs())
.unwrap_or(false),
)
});
self.debug_sessions.push_back(DebugAdapterState::new(
session_id,
adapter_name,
has_adapter_logs,
));
self.clean_sessions(cx);
client_id: SessionId,
client: Entity<Session>,
cx: &App,
) -> Option<&mut DebugAdapterState> {
let client_state = self
.debug_clients
.entry(client_id)
.or_insert_with(DebugAdapterState::new);
let io_tx = self.rpc_tx.clone();
let Some(client) = session.read(cx).adapter_client() else {
return;
};
let client = client.read(cx).adapter_client()?;
client.add_log_handler(
move |io_kind, command, message| {
move |io_kind, message| {
io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.unbounded_send((client_id, io_kind, message.to_string()))
.ok();
},
LogKind::Rpc,
@@ -374,66 +309,34 @@ impl LogStore {
let log_io_tx = self.adapter_log_tx.clone();
client.add_log_handler(
move |io_kind, command, message| {
move |io_kind, message| {
log_io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.unbounded_send((client_id, io_kind, message.to_string()))
.ok();
},
LogKind::Adapter,
);
Some(client_state)
}
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
self.debug_sessions.retain(|session| {
if to_remove > 0 && session.is_terminated {
to_remove -= 1;
return false;
}
true
});
fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
self.debug_clients.remove(&client_id);
cx.notify();
}
fn log_messages_for_session(
&mut self,
session_id: SessionId,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions
.iter_mut()
.find(|session| session.id == session_id)
.map(|state| &mut state.log_messages)
fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
}
fn rpc_messages_for_session(
&mut self,
session_id: SessionId,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.messages)
} else {
None
}
})
}
fn initialization_sequence_for_session(
&mut self,
session_id: SessionId,
) -> Option<&mut Vec<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.initialization_sequence)
} else {
None
}
})
fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
Some(
&mut self
.debug_clients
.get_mut(&client_id)?
.rpc_messages
.messages,
)
}
}
@@ -453,15 +356,18 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element();
};
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {
(
log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id),
log_view.menu_items(cx).unwrap_or_default(),
log_view.current_view.map(|(client_id, _)| client_id),
)
});
let current_client = current_session_id
.and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
let current_client = current_client_id.and_then(|current_client_id| {
menu_rows
.iter()
.find(|row| row.client_id == current_client_id)
});
let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
.anchor(gpui::Corner::TopLeft)
@@ -471,8 +377,8 @@ impl Render for DapLogToolbarItemView {
.map(|sub_item| {
Cow::Owned(format!(
"{} ({}) - {}",
sub_item.adapter_name,
sub_item.session_id.0,
sub_item.client_name,
sub_item.client_id.0,
match sub_item.selected_entry {
LogKind::Adapter => ADAPTER_LOGS,
LogKind::Rpc => RPC_MESSAGES,
@@ -491,10 +397,9 @@ impl Render for DapLogToolbarItemView {
.w_full()
.pl_2()
.child(
Label::new(format!(
"{}. {}",
row.session_id.0, row.adapter_name,
))
Label::new(
format!("{}. {}", row.client_id.0, row.client_name,),
)
.color(workspace::ui::Color::Muted),
)
.into_any_element()
@@ -510,40 +415,23 @@ impl Render for DapLogToolbarItemView {
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_log_messages_for_adapter(row.session_id, window, cx);
view.show_log_messages_for_adapter(row.client_id, window, cx);
}),
);
}
menu = menu
.custom_entry(
move |_window, _cx| {
div()
.w_full()
.pl_4()
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_rpc_trace_for_server(row.session_id, window, cx);
}),
)
.custom_entry(
move |_window, _cx| {
div()
.w_full()
.pl_4()
.child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_initialization_sequence_for_server(
row.session_id,
window,
cx,
);
}),
);
menu = menu.custom_entry(
move |_window, _cx| {
div()
.w_full()
.pl_4()
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_rpc_trace_for_server(row.client_id, window, cx);
}),
);
}
menu
@@ -630,13 +518,7 @@ impl DapLogView {
}
});
let state_info = log_store
.read(cx)
.debug_sessions
.back()
.map(|session| (session.id, session.has_adapter_logs));
let mut this = Self {
Self {
editor,
focus_handle,
project,
@@ -644,17 +526,7 @@ impl DapLogView {
editor_subscriptions,
current_view: None,
_subscriptions: vec![events_subscriptions],
};
if let Some((session_id, have_adapter_logs)) = state_info {
if have_adapter_logs {
this.show_log_messages_for_adapter(session_id, window, cx);
} else {
this.show_rpc_trace_for_server(session_id, window, cx);
}
}
this
}
fn editor_for_logs(
@@ -687,34 +559,42 @@ impl DapLogView {
(editor, vec![editor_subscription, search_subscription])
}
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
self.log_store
fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
let mut menu_items = self
.project
.read(cx)
.debug_sessions
.iter()
.rev()
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
.dap_store()
.read(cx)
.sessions()
.filter_map(|session| {
let session = session.read(cx);
session.adapter();
let client = session.adapter_client()?;
Some(DapMenuItem {
client_id: client.id(),
client_name: session.adapter().to_string(),
has_adapter_logs: client.has_adapter_logs(),
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
})
})
.collect::<Vec<_>>()
.collect::<Vec<_>>();
menu_items.sort_by_key(|item| item.client_id.0);
Some(menu_items)
}
fn show_rpc_trace_for_server(
&mut self,
session_id: SessionId,
client_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.rpc_messages_for_session(session_id)
.map(|state| log_contents(state.iter().cloned()))
.rpc_messages_for_client(client_id)
.map(|state| log_contents(&state))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
self.current_view = Some((client_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -746,17 +626,17 @@ impl DapLogView {
fn show_log_messages_for_adapter(
&mut self,
session_id: SessionId,
client_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let message_log = self.log_store.update(cx, |log_store, _| {
log_store
.log_messages_for_session(session_id)
.map(|state| log_contents(state.iter().cloned()))
.log_messages_for_client(client_id)
.map(|state| log_contents(&state))
});
if let Some(message_log) = message_log {
self.current_view = Some((session_id, LogKind::Adapter));
self.current_view = Some((client_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -772,53 +652,14 @@ impl DapLogView {
cx.focus_self(window);
}
fn show_initialization_sequence_for_server(
&mut self,
session_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.initialization_sequence_for_session(session_id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
let buffer = cx.entity();
async move |_, cx| {
let language = language.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
})
}
})
.detach_and_log_err(cx);
});
self.editor = editor;
self.editor_subscriptions = editor_subscriptions;
cx.notify();
}
cx.focus_self(window);
}
}
fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
lines.fold(String::new(), |mut acc, el| {
acc.push_str(&el);
fn log_contents(lines: &VecDeque<String>) -> String {
let (a, b) = lines.as_slices();
let a = a.iter().map(move |v| v.as_ref());
let b = b.iter().map(move |v| v.as_ref());
a.chain(b).fold(String::new(), |mut acc, el| {
acc.push_str(el);
acc.push('\n');
acc
})
@@ -826,15 +667,14 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
#[derive(Clone, PartialEq)]
pub(crate) struct DapMenuItem {
pub session_id: SessionId,
pub adapter_name: DebugAdapterName,
pub client_id: SessionId,
pub client_name: String,
pub has_adapter_logs: bool,
pub selected_entry: LogKind,
}
const ADAPTER_LOGS: &str = "Adapter Logs";
const RPC_MESSAGES: &str = "RPC Messages";
const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
impl Render for DapLogView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -996,7 +836,7 @@ impl Focusable for DapLogView {
pub enum Event {
NewLogEntry {
id: SessionId,
entry: SharedString,
entry: String,
kind: LogKind,
},
}
@@ -1009,16 +849,12 @@ impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
pub fn contained_session_ids(&self) -> Vec<SessionId> {
self.debug_sessions
.iter()
.map(|session| session.id)
.collect()
self.debug_clients.keys().cloned().collect()
}
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
self.debug_clients
.get(&session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
@@ -1026,10 +862,9 @@ impl LogStore {
.into()
}
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
self.debug_clients
.get(&session_id)
.expect("This session should exist if a test is calling")
.log_messages
.clone()

View File

@@ -2,23 +2,25 @@ use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
persistence, spawn_task_or_modal,
};
use anyhow::Result;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
WeakEntity, actions, anchored, deferred,
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
actions, anchored, deferred,
};
use language::Buffer;
@@ -27,10 +29,10 @@ use project::{Fs, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::maybe;
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -138,6 +140,82 @@ impl DebugPanel {
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self
.active_session()
.map(|item| {
let running = item.read(cx).running_state().clone();
let caps = running.read(cx).capabilities(cx);
(
!running.read(cx).session().read(cx).is_terminated(),
caps.supports_restart_request.unwrap_or_default(),
caps.supports_step_back.unwrap_or_default(),
running.read(cx).thread_status(cx),
)
})
.unwrap_or((false, false, false, None));
let filter = CommandPaletteFilter::global_mut(cx);
let debugger_action_types = [
TypeId::of::<Detach>(),
TypeId::of::<Stop>(),
TypeId::of::<ToggleIgnoreBreakpoints>(),
];
let running_action_types = [TypeId::of::<Pause>()];
let stopped_action_type = [
TypeId::of::<Continue>(),
TypeId::of::<StepOver>(),
TypeId::of::<StepInto>(),
TypeId::of::<StepOut>(),
TypeId::of::<ShowStackTrace>(),
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
];
let step_back_action_type = [TypeId::of::<StepBack>()];
let restart_action_type = [TypeId::of::<Restart>()];
if has_active_session {
filter.show_action_types(debugger_action_types.iter());
if supports_restart {
filter.show_action_types(restart_action_type.iter());
} else {
filter.hide_action_types(&restart_action_type);
}
if support_step_back {
filter.show_action_types(step_back_action_type.iter());
} else {
filter.hide_action_types(&step_back_action_type);
}
match status {
Some(ThreadStatus::Running) => {
filter.show_action_types(running_action_types.iter());
filter.hide_action_types(&stopped_action_type);
}
Some(ThreadStatus::Stopped) => {
filter.show_action_types(stopped_action_type.iter());
filter.hide_action_types(&running_action_types);
}
_ => {
filter.hide_action_types(&running_action_types);
filter.hide_action_types(&stopped_action_type);
}
}
} else {
// show only the `debug: start`
filter.hide_action_types(&debugger_action_types);
filter.hide_action_types(&step_back_action_type);
filter.hide_action_types(&restart_action_type);
filter.hide_action_types(&running_action_types);
filter.hide_action_types(&stopped_action_type);
}
}
pub fn load(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
@@ -155,6 +233,17 @@ impl DebugPanel {
)
});
cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
Self::filter_action_types(debug_panel, cx);
})
.detach();
cx.observe(&debug_panel, |_, debug_panel, cx| {
debug_panel.update(cx, |debug_panel, cx| {
Self::filter_action_types(debug_panel, cx);
});
})
.detach();
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
debug_panel
@@ -176,24 +265,10 @@ impl DebugPanel {
dap_store.new_session(
scenario.label.clone(),
DebugAdapterName(scenario.adapter.clone()),
task_context.clone(),
None,
cx,
)
});
let worktree = worktree_id.or_else(|| {
active_buffer
.as_ref()
.and_then(|buffer| buffer.read(cx).file())
.map(|f| f.worktree_id(cx))
});
let Some(worktree) = worktree
.and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
.or_else(|| self.project.read(cx).visible_worktrees(cx).next())
else {
log::debug!("Could not find a worktree to spawn the debug session in");
return;
};
self.debug_scenario_scheduled_last = true;
if let Some(inventory) = self
.project
@@ -228,7 +303,7 @@ impl DebugPanel {
.await?;
dap_store
.update(cx, |dap_store, cx| {
dap_store.boot_session(session.clone(), definition, worktree, cx)
dap_store.boot_session(session.clone(), definition, cx)
})?
.await
}
@@ -337,41 +412,22 @@ impl DebugPanel {
let dap_store_handle = self.project.read(cx).dap_store().clone();
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 binary = curr_session.read(cx).binary().clone();
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let task_context = curr_session.read(cx).task_context().clone();
cx.spawn_in(window, async move |this, cx| {
task.await;
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session = dap_store.new_session(label, adapter, task_context, None, cx);
let session = dap_store.new_session(label, adapter, None, cx);
let task = session.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
});
(session, task)
})?;
Self::register_session(this.clone(), session.clone(), true, cx).await?;
if let Err(error) = task.await {
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!(
"Session failed to restart with error: {}",
error
))
.ok();
session.shutdown(cx)
})?
.await;
return Err(error);
};
Ok(())
Self::register_session(this.clone(), session, true, cx).await?;
task.await
})
.detach_and_log_err(cx);
}
@@ -389,34 +445,24 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = self.label_for_child_session(&parent_session, request, cx);
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
let adapter = parent_session.read(cx).adapter().clone();
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
log::error!("Attempted to start a child-session without a binary");
return;
};
let task_context = parent_session.read(cx).task_context().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
cx.spawn_in(window, async move |this, cx| {
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session = dap_store.new_session(
label,
adapter,
task_context,
Some(parent_session.clone()),
cx,
);
let session =
dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
let task = session.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
});
(session, task)
})?;
// Focus child sessions if the parent has never emitted a stopped event;
// this improves our JavaScript experience, as it always spawns a "main" session that then spawns subsessions.
let parent_ever_stopped =
parent_session.update(cx, |this, _| this.has_ever_stopped())?;
Self::register_session(this, session, !parent_ever_stopped, cx).await?;
Self::register_session(this, session, false, cx).await?;
task.await
})
.detach_and_log_err(cx);
@@ -533,26 +579,6 @@ impl DebugPanel {
}
}
fn copy_debug_adapter_arguments(
&mut self,
_: &CopyDebugAdapterArguments,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let content = maybe!({
let mut session = self.active_session()?.read(cx).session(cx);
while let Some(parent) = session.read(cx).parent_session().cloned() {
session = parent;
}
let binary = session.read(cx).binary()?;
let content = serde_json::to_string_pretty(&binary).ok()?;
Some(content)
});
if let Some(content) = content {
cx.write_to_clipboard(ClipboardItem::new_string(content));
}
}
pub(crate) fn top_controls_strip(
&mut self,
window: &mut Window,
@@ -582,12 +608,6 @@ impl DebugPanel {
}
})
};
let documentation_button = || {
IconButton::new("debug-open-documentation", IconName::CircleHelp)
.icon_size(IconSize::Small)
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
.tooltip(Tooltip::text("Open Documentation"))
};
Some(
div.border_b_1()
@@ -609,8 +629,6 @@ impl DebugPanel {
project::debugger::session::ThreadStatus::Exited,
);
let capabilities = running_state.read(cx).capabilities(cx);
let supports_detach =
running_state.read(cx).session().read(cx).is_attached();
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -799,48 +817,33 @@ impl DebugPanel {
}
}),
)
.when(
supports_detach,
|div| {
div.child(
IconButton::new(
"debug-disconnect",
IconName::DebugDetach,
)
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Detach",
&Detach,
&focus_handle,
window,
cx,
)
}
}),
)
},
.child(
IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Detach",
&Detach,
&focus_handle,
window,
cx,
)
}
}),
)
},
),
)
.justify_around()
.when(is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
.when(is_side, |this| this.child(new_session_button())),
)
.child(
h_flex()
@@ -881,10 +884,7 @@ impl DebugPanel {
window,
cx,
))
.when(!is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
.when(!is_side, |this| this.child(new_session_button())),
),
),
)
@@ -1039,25 +1039,6 @@ impl DebugPanel {
cx.emit(PanelEvent::ZoomIn);
}
}
fn label_for_child_session(
&self,
parent_session: &Entity<Session>,
request: &StartDebuggingRequestArguments,
cx: &mut Context<'_, Self>,
) -> SharedString {
let adapter = parent_session.read(cx).adapter();
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
if let Some(label) = adapter.label_for_child_session(request) {
return label.into();
}
}
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
label
}
}
async fn register_session_inner(
@@ -1085,11 +1066,6 @@ async fn register_session_inner(
.ok();
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
let parent_session = this
.sessions
.iter()
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
.cloned();
this.sessions.retain(|session| {
!session
.read(cx)
@@ -1103,8 +1079,8 @@ async fn register_session_inner(
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
@@ -1384,7 +1360,6 @@ impl Render for DebugPanel {
});
cx.notify();
}))
.on_action(cx.listener(Self::copy_debug_adapter_arguments))
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,

View File

@@ -1,17 +1,14 @@
use std::any::TypeId;
use dap::debugger_settings::DebuggerSettings;
use debugger_panel::{DebugPanel, ToggleFocus};
use editor::Editor;
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
use gpui::{App, DispatchPhase, EntityInputHandler, actions};
use gpui::{App, EntityInputHandler, actions};
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
use stack_trace_view::StackTraceView;
use tasks_ui::{Spawn, TaskOverrides};
use ui::{FluentBuilder, InteractiveElement};
use util::maybe;
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
@@ -56,8 +53,6 @@ actions!(
]
);
actions!(dev, [CopyDebugAdapterArguments]);
pub fn init(cx: &mut App) {
DebuggerSettings::register(cx);
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
@@ -73,6 +68,148 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
.register_action(|workspace, _: &Pause, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.pause_thread(cx))
}
}
})
.register_action(|workspace, _: &Restart, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.restart_session(cx))
}
}
})
.register_action(|workspace, _: &Continue, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.continue_thread(cx))
}
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_in(cx))
}
}
})
.register_action(|workspace, _: &StepOver, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_over(cx))
}
}
})
.register_action(|workspace, _: &StepOut, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session()
.map(|session| session.read(cx).running_state().clone())
}) {
active_item.update(cx, |item, cx| item.step_out(cx))
}
}
})
.register_action(|workspace, _: &StepBack, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_back(cx))
}
}
})
.register_action(|workspace, _: &Stop, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
cx.defer(move |cx| {
active_item.update(cx, |item, cx| item.stop_thread(cx))
})
}
}
})
.register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
}
}
})
.register_action(
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |store, cx| {
store.shutdown_sessions(cx).detach();
})
})
},
)
.register_action(
|workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let Some(active_session) = debug_panel.read(cx).active_session() else {
return;
};
let project = workspace.project();
let stack_trace_view = active_session.update(cx, |session, cx| {
session.stack_trace_view(project, window, cx).clone()
});
workspace.add_item_to_active_pane(
Box::new(stack_trace_view),
None,
true,
window,
cx,
);
}
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
@@ -86,254 +223,90 @@ pub fn init(cx: &mut App) {
debug_panel.rerun_last_session(workspace, window, cx);
})
},
)
.register_action(
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |store, cx| {
store.shutdown_sessions(cx).detach();
})
})
},
)
.register_action_renderer(|div, workspace, _, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return div;
};
let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
else {
return div;
};
let running_state = active_item.read(cx);
if running_state.session().read(cx).is_terminated() {
return div;
}
let caps = running_state.capabilities(cx);
let supports_step_back = caps.supports_step_back.unwrap_or_default();
let supports_detach = running_state.session().read(cx).is_attached();
let status = running_state.thread_status(cx);
let active_item = active_item.downgrade();
div.when(status == Some(ThreadStatus::Running), |div| {
let active_item = active_item.clone();
div.on_action(move |_: &Pause, _, cx| {
active_item
.update(cx, |item, cx| item.pause_thread(cx))
.ok();
})
})
.when(status == Some(ThreadStatus::Stopped), |div| {
div.on_action({
let active_item = active_item.clone();
move |_: &StepInto, _, cx| {
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &StepOver, _, cx| {
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &StepOut, _, cx| {
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
}
})
.when(supports_step_back, |div| {
let active_item = active_item.clone();
div.on_action(move |_: &StepBack, _, cx| {
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
})
})
.on_action({
let active_item = active_item.clone();
move |_: &Continue, _, cx| {
active_item
.update(cx, |item, cx| item.continue_thread(cx))
.ok();
}
})
.on_action(cx.listener(
|workspace, _: &ShowStackTrace, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
{
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace
.activate_item(&existing, true, !is_active, window, cx);
} else {
let Some(active_session) =
debug_panel.read(cx).active_session()
else {
return;
};
let project = workspace.project();
let stack_trace_view =
active_session.update(cx, |session, cx| {
session.stack_trace_view(project, window, cx).clone()
});
workspace.add_item_to_active_pane(
Box::new(stack_trace_view),
None,
true,
window,
cx,
);
}
},
))
})
.when(supports_detach, |div| {
let active_item = active_item.clone();
div.on_action(move |_: &Detach, _, cx| {
active_item
.update(cx, |item, cx| item.detach_client(cx))
.ok();
})
})
.on_action({
let active_item = active_item.clone();
move |_: &Restart, _, cx| {
active_item
.update(cx, |item, cx| item.restart_session(cx))
.ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &Stop, _, cx| {
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &ToggleIgnoreBreakpoints, _, cx| {
active_item
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
.ok();
}
})
});
);
})
})
.detach();
cx.observe_new({
move |editor: &mut Editor, _, _| {
move |editor: &mut Editor, _, cx| {
editor
.register_action_renderer(move |editor, window, cx| {
let Some(workspace) = editor.workspace() else {
return;
};
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
return;
};
let Some(active_session) = debug_panel
.clone()
.update(cx, |panel, _| panel.active_session())
else {
return;
};
let editor = cx.entity().downgrade();
window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
let editor = editor.clone();
let active_session = active_session.clone();
move |_, phase, _, cx| {
if phase != DispatchPhase::Bubble {
return;
}
maybe!({
let (buffer, position, _) = editor
.update(cx, |editor, cx| {
let cursor_point: language::Point =
editor.selections.newest(cx).head();
.register_action(cx.listener(
move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
maybe!({
let debug_panel =
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
let cursor_point: language::Point = editor.selections.newest(cx).head();
let active_session = debug_panel.read(cx).active_session()?;
editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)
})
.ok()??;
let (buffer, position, _) = editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)?;
let path =
let path =
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
&buffer, cx,
)?;
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
});
});
Some(())
});
}
});
window.on_action(
TypeId::of::<editor::actions::EvaluateSelectedText>(),
move |_, _, window, cx| {
maybe!({
let text = editor
.update(cx, |editor, cx| {
editor.text_for_range(
editor.selections.newest(cx).range(),
&mut None,
window,
cx,
)
})
.ok()??;
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
let stack_id = state.selected_stack_frame_id(cx);
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session
.evaluate(text, None, stack_id, None, cx)
.detach();
});
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
});
});
Some(())
});
},
))
.detach();
editor
.register_action(cx.listener(
move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
maybe!({
let debug_panel =
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
let active_session = debug_panel.read(cx).active_session()?;
let text = editor.text_for_range(
editor.selections.newest(cx).range(),
&mut None,
window,
cx,
)?;
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
let stack_id = state.selected_stack_frame_id(cx);
state.session().update(cx, |session, cx| {
session.evaluate(text, None, stack_id, None, cx).detach();
});
});
Some(())
});
},
);
})
Some(())
});
},
))
.detach();
}
})

Some files were not shown because too many files have changed in this diff Show More