Compare commits
54 Commits
breakpoint
...
fix-text-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a7019049 | ||
|
|
0e35f5bf20 | ||
|
|
7ac19ff1ed | ||
|
|
804066a047 | ||
|
|
4a356466b1 | ||
|
|
0921762b59 | ||
|
|
46b1df2e2d | ||
|
|
986da332db | ||
|
|
dad33f7cc2 | ||
|
|
64241f7d2f | ||
|
|
fbbc23bec3 | ||
|
|
26f4705198 | ||
|
|
3abf95216c | ||
|
|
b0b52f299c | ||
|
|
53cde329da | ||
|
|
b55b310ad0 | ||
|
|
8ab25e2bac | ||
|
|
c10b1f7c61 | ||
|
|
cb1ee01a66 | ||
|
|
90bcde116f | ||
|
|
8ac378b86e | ||
|
|
55760295d9 | ||
|
|
9dfb907f97 | ||
|
|
e20daa7639 | ||
|
|
b46ab367ef | ||
|
|
12212dc329 | ||
|
|
324e4658ba | ||
|
|
ed500dacb6 | ||
|
|
2f4b48129b | ||
|
|
ed7c55a04e | ||
|
|
6db4ab381c | ||
|
|
0e72a7e6ce | ||
|
|
3dc3ab062d | ||
|
|
ed63f216e3 | ||
|
|
ba767a1998 | ||
|
|
23c3f5f410 | ||
|
|
b3be294c90 | ||
|
|
af5318df98 | ||
|
|
60c420a2da | ||
|
|
ee6c33ffb3 | ||
|
|
9ae4f4b158 | ||
|
|
915a1cb116 | ||
|
|
aead0e11ff | ||
|
|
7d19c452b4 | ||
|
|
2b76bd8e35 | ||
|
|
070464e4c5 | ||
|
|
69be667c02 | ||
|
|
0155974e59 | ||
|
|
575782ea88 | ||
|
|
0d6b2052ea | ||
|
|
1e16283820 | ||
|
|
0a61f38231 | ||
|
|
1e2b16a1a3 | ||
|
|
cb21b53ca9 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -225,7 +225,7 @@ jobs:
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"label": "Debug Zed with LLDB",
|
||||
"adapter": "LLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"label": "Debug Zed with GDB",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
|
||||
106
Cargo.lock
generated
106
Cargo.lock
generated
@@ -65,6 +65,7 @@ dependencies = [
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"context_server",
|
||||
"convert_case 0.8.0",
|
||||
"db",
|
||||
@@ -85,6 +86,7 @@ dependencies = [
|
||||
"language",
|
||||
"language_model",
|
||||
"language_model_selector",
|
||||
"linkme",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
@@ -1639,7 +1641,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -4901,6 +4903,37 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eval"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"dap",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_models",
|
||||
"node_runtime",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"settings",
|
||||
"smol",
|
||||
"toml 0.8.20",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evals"
|
||||
version = "0.1.0"
|
||||
@@ -6636,7 +6669,7 @@ dependencies = [
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-platform-verifier",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -6735,7 +6768,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -7080,9 +7113,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
@@ -7188,9 +7221,12 @@ name = "install_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"gpui",
|
||||
"release_channel",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -7937,9 +7973,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8610,9 +8646,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.45"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -10950,9 +10986,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.4"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1"
|
||||
checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fnv",
|
||||
@@ -10960,7 +10996,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"parking_lot",
|
||||
"protobuf",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11135,9 +11171,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "2.28.0"
|
||||
version = "3.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||
checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"protobuf-support",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-support"
|
||||
version = "3.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
|
||||
dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
@@ -11263,7 +11313,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"socket2",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
@@ -11282,7 +11332,7 @@ dependencies = [
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.12",
|
||||
@@ -11890,7 +11940,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
@@ -12281,9 +12331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.25"
|
||||
version = "0.23.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -12357,7 +12407,7 @@ dependencies = [
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.1",
|
||||
@@ -13222,9 +13272,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -13455,7 +13505,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14750,7 +14800,7 @@ version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
|
||||
dependencies = [
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -14810,7 +14860,7 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
@@ -15369,7 +15419,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.0",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
@@ -17717,7 +17767,7 @@ dependencies = [
|
||||
"rust_decimal",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.0.5",
|
||||
"rustls 0.23.25",
|
||||
"rustls 0.23.26",
|
||||
"rustls-webpki 0.103.1",
|
||||
"scopeguard",
|
||||
"sea-orm",
|
||||
@@ -18106,7 +18156,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.182.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
@@ -47,6 +47,7 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/evals",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
@@ -509,7 +510,7 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.22" }
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
|
||||
1
assets/icons/binary.svg
Normal file
1
assets/icons/binary.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-binary-icon lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>
|
||||
|
After Width: | Height: | Size: 413 B |
1
assets/icons/flame.svg
Normal file
1
assets/icons/flame.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-flame-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 415 B |
1
assets/icons/function.svg
Normal file
1
assets/icons/function.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -644,7 +644,6 @@
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewPromptEditor",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
@@ -660,7 +659,6 @@
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
@@ -669,7 +667,6 @@
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
@@ -801,7 +798,6 @@
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
|
||||
@@ -163,3 +163,8 @@ There are rules that apply to these root directories:
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<user_environment>
|
||||
Operating System: {{os}} ({{arch}})
|
||||
Shell: {{shell}}
|
||||
</user_environment>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
In your response, make sure to remember and follow my instructions about how to format code blocks (and don't mention that you are remembering it, just follow the instructions).
|
||||
@@ -656,8 +656,9 @@
|
||||
"name": "Write",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"terminal": true,
|
||||
"batch_tool": true,
|
||||
"code_actions": true,
|
||||
"code_symbols": true,
|
||||
"copy_path": false,
|
||||
"create_file": true,
|
||||
@@ -671,6 +672,7 @@
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
@@ -51,6 +52,7 @@ itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -85,9 +87,9 @@ ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::{AssistantContext, ContextId};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
@@ -8,6 +7,7 @@ use crate::thread::{
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -21,7 +21,7 @@ use gpui::{
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::parser::CodeBlockKind;
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
|
||||
use project::ProjectItem as _;
|
||||
@@ -57,6 +57,7 @@ pub struct ActiveThread {
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||
expanded_code_blocks: HashMap<(MessageId, usize), bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
||||
@@ -297,7 +298,7 @@ fn render_markdown_code_block(
|
||||
codeblock_range: Range<usize>,
|
||||
active_thread: Entity<ActiveThread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
_window: &Window,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
let label = match kind {
|
||||
@@ -377,16 +378,20 @@ fn render_markdown_code_block(
|
||||
.rounded_sm()
|
||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path_range = path_range.clone();
|
||||
@@ -444,16 +449,32 @@ fn render_markdown_code_block(
|
||||
}),
|
||||
};
|
||||
|
||||
let codeblock_was_copied = active_thread
|
||||
.read(cx)
|
||||
.copied_code_block_ids
|
||||
.contains(&(message_id, ix));
|
||||
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, ix))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
let codeblock_header_bg = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
||||
|
||||
let codeblock_was_copied = active_thread
|
||||
.read(cx)
|
||||
.copied_code_block_ids
|
||||
.contains(&(message_id, ix));
|
||||
const CODE_FENCES_LINE_COUNT: usize = 2;
|
||||
const MAX_COLLAPSED_LINES: usize = 5;
|
||||
|
||||
let line_count = parsed_markdown.source()[codeblock_range.clone()]
|
||||
.bytes()
|
||||
.filter(|c| *c == b'\n')
|
||||
.count()
|
||||
.saturating_sub(CODE_FENCES_LINE_COUNT - 1);
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.group("codeblock_header")
|
||||
@@ -466,57 +487,104 @@ fn render_markdown_code_block(
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
.child(
|
||||
div().visible_on_hover("codeblock_header").child(
|
||||
IconButton::new(
|
||||
("copy-markdown-code", ix),
|
||||
if codeblock_was_copied {
|
||||
IconName::Check
|
||||
} else {
|
||||
IconName::Copy
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
let parsed_markdown = parsed_markdown.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids.insert((message_id, ix));
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div().visible_on_hover("codeblock_header").child(
|
||||
IconButton::new(
|
||||
("copy-markdown-code", ix),
|
||||
if codeblock_was_copied {
|
||||
IconName::Check
|
||||
} else {
|
||||
IconName::Copy
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
let parsed_markdown = parsed_markdown.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids.insert((message_id, ix));
|
||||
|
||||
let code =
|
||||
without_fences(&parsed_markdown.source()[codeblock_range.clone()])
|
||||
let code = without_fences(
|
||||
&parsed_markdown.source()[codeblock_range.clone()],
|
||||
)
|
||||
.to_string();
|
||||
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(2))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids.remove(&(message_id, ix));
|
||||
cx.notify();
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids
|
||||
.remove(&(message_id, ix));
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(line_count > MAX_COLLAPSED_LINES, |header| {
|
||||
header.child(
|
||||
IconButton::new(
|
||||
("expand-collapse-code", ix),
|
||||
if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text(if is_expanded {
|
||||
"Collapse Code"
|
||||
} else {
|
||||
"Expand Code"
|
||||
}))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
let is_expanded = this
|
||||
.expanded_code_blocks
|
||||
.entry((message_id, ix))
|
||||
.or_insert(false);
|
||||
*is_expanded = !*is_expanded;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.relative()
|
||||
.my_2()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(codeblock_header)
|
||||
.when(line_count > MAX_COLLAPSED_LINES, |this| {
|
||||
if is_expanded {
|
||||
this.h_full()
|
||||
} else {
|
||||
this.max_h_40()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn open_markdown_link(
|
||||
@@ -626,6 +694,7 @@ impl ActiveThread {
|
||||
rendered_tool_uses: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
expanded_code_blocks: HashMap::default(),
|
||||
list_state: list_state.clone(),
|
||||
scrollbar_state: ScrollbarState::new(list_state),
|
||||
show_scrollbar: false,
|
||||
@@ -828,11 +897,7 @@ impl ActiveThread {
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let tool_uses = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.use_pending_tools(cx));
|
||||
|
||||
ThreadEvent::UsePendingTools { tool_uses } => {
|
||||
for tool_use in tool_uses {
|
||||
self.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
@@ -844,11 +909,8 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
pending_tool_use,
|
||||
canceled,
|
||||
..
|
||||
pending_tool_use, ..
|
||||
} => {
|
||||
let canceled = *canceled;
|
||||
if let Some(tool_use) = pending_tool_use {
|
||||
self.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
@@ -862,18 +924,6 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if self.thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.attach_tool_results(cx);
|
||||
if !canceled {
|
||||
thread.send_to_model(model, RequestKind::Chat, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
}
|
||||
@@ -1314,8 +1364,16 @@ impl ActiveThread {
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
|
||||
});
|
||||
|
||||
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
@@ -1367,7 +1425,8 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(open_as_markdown),
|
||||
)
|
||||
.into_any_element(),
|
||||
None => feedback_container
|
||||
@@ -1380,6 +1439,7 @@ impl ActiveThread {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
|
||||
@@ -1410,7 +1470,8 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(open_as_markdown),
|
||||
)
|
||||
.into_any_element(),
|
||||
};
|
||||
@@ -1835,10 +1896,10 @@ impl ActiveThread {
|
||||
render: Arc::new({
|
||||
let workspace = workspace.clone();
|
||||
let active_thread = cx.entity();
|
||||
move |id, kind, parsed_markdown, range, window, cx| {
|
||||
move |kind, parsed_markdown, range, window, cx| {
|
||||
render_markdown_code_block(
|
||||
message_id,
|
||||
id,
|
||||
range.start,
|
||||
kind,
|
||||
parsed_markdown,
|
||||
range,
|
||||
@@ -1849,6 +1910,44 @@ impl ActiveThread {
|
||||
)
|
||||
}
|
||||
}),
|
||||
transform: Some(Arc::new({
|
||||
let active_thread = cx.entity();
|
||||
move |el, range, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_expanded {
|
||||
return el;
|
||||
}
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_1_4()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().editor_background,
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
})),
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -2840,10 +2939,10 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let path = directory_context.project_path.clone();
|
||||
let project_path = directory_context.project_path(cx);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
if let Some(entry) = project.entry_for_path(&path, cx) {
|
||||
if let Some(entry) = project.entry_for_path(&project_path, cx) {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -863,7 +863,11 @@ impl AssistantPanel {
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
} else {
|
||||
change_title_editor.clone().into_any_element()
|
||||
div()
|
||||
.ml_2()
|
||||
.w_full()
|
||||
.child(change_title_editor.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::ProjectPath;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
@@ -69,10 +69,21 @@ pub struct FileContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub id: ContextId,
|
||||
pub project_path: ProjectPath,
|
||||
pub worktree: Entity<Worktree>,
|
||||
pub path: Arc<Path>,
|
||||
/// Buffers of the files within the directory.
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn project_path(&self, cx: &App) -> ProjectPath {
|
||||
ProjectPath {
|
||||
worktree_id: self.worktree.read(cx).id(),
|
||||
path: self.path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub id: ContextId,
|
||||
@@ -86,12 +97,11 @@ pub struct FetchedUrlContext {
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
// TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
|
||||
// a WeakEntity and handle removal from the UI when it has dropped.
|
||||
pub thread: Entity<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -105,12 +115,11 @@ impl ThreadContext {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
|
||||
// a WeakEntity and handle removal from the UI when it has dropped.
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub file: Arc<dyn File>,
|
||||
pub version: clock::Global,
|
||||
|
||||
@@ -289,12 +289,14 @@ impl ContextPicker {
|
||||
path_prefix,
|
||||
} => {
|
||||
let context_store = self.context_store.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let path = project_path.path.clone();
|
||||
|
||||
ContextMenuItem::custom_entry(
|
||||
move |_window, cx| {
|
||||
render_file_context_entry(
|
||||
ElementId::NamedInteger("ctx-recent".into(), ix),
|
||||
worktree_id,
|
||||
&path,
|
||||
&path_prefix,
|
||||
false,
|
||||
@@ -466,7 +468,7 @@ fn recent_context_picker_entries(
|
||||
recent.extend(
|
||||
workspace
|
||||
.recent_navigation_history_iter(cx)
|
||||
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||
.filter(|(path, _)| !current_files.contains(path))
|
||||
.take(4)
|
||||
.filter_map(|(project_path, _)| {
|
||||
project
|
||||
|
||||
@@ -189,6 +189,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.toggle_state(selected)
|
||||
.child(render_file_context_entry(
|
||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||
WorktreeId::from_usize(mat.worktree_id),
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
mat.is_dir,
|
||||
@@ -328,19 +329,26 @@ pub fn extract_file_name_and_directory(
|
||||
|
||||
pub fn render_file_context_entry(
|
||||
id: ElementId,
|
||||
path: &Path,
|
||||
worktree_id: WorktreeId,
|
||||
path: &Arc<Path>,
|
||||
path_prefix: &Arc<str>,
|
||||
is_directory: bool,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
|
||||
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
if is_directory {
|
||||
context_store.read(cx).includes_directory(path)
|
||||
context_store.read(cx).includes_directory(&project_path)
|
||||
} else {
|
||||
context_store.read(cx).will_include_file_path(path, cx)
|
||||
context_store
|
||||
.read(cx)
|
||||
.will_include_file_path(&project_path, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -380,8 +388,9 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
FileInclusion::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
FileInclusion::InDirectory(directory_project_path) => {
|
||||
// TODO: Consider using worktree full_path to include worktree name.
|
||||
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
|
||||
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -395,7 +404,7 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Included").size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(Tooltip::text(format!("in {dir_name}")))
|
||||
.tooltip(Tooltip::text(format!("in {directory_path}")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -28,7 +28,7 @@ pub struct ContextStore {
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
next_context_id: ContextId,
|
||||
files: BTreeMap<BufferId, ContextId>,
|
||||
directories: HashMap<PathBuf, ContextId>,
|
||||
directories: HashMap<ProjectPath, ContextId>,
|
||||
symbols: HashMap<ContextSymbolId, ContextId>,
|
||||
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
|
||||
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
|
||||
@@ -93,7 +93,7 @@ impl ContextStore {
|
||||
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
|
||||
|
||||
let already_included = this.update(cx, |this, cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
match this.will_include_buffer(buffer_id, &project_path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
this.remove_context(context_id, cx);
|
||||
@@ -159,7 +159,7 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let already_included = match self.includes_directory(&project_path.path) {
|
||||
let already_included = match self.includes_directory(&project_path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id, cx);
|
||||
@@ -223,14 +223,12 @@ impl ContextStore {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if context_buffers.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No text files found in {}",
|
||||
&project_path.path.display()
|
||||
));
|
||||
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
|
||||
return Err(anyhow!("No text files found in {}", &full_path.display()));
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_directory(project_path, context_buffers, cx);
|
||||
this.insert_directory(worktree, project_path, context_buffers, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -239,17 +237,20 @@ impl ContextStore {
|
||||
|
||||
fn insert_directory(
|
||||
&mut self,
|
||||
worktree: Entity<Worktree>,
|
||||
project_path: ProjectPath,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(project_path.path.to_path_buf(), id);
|
||||
let path = project_path.path.clone();
|
||||
self.directories.insert(project_path, id);
|
||||
|
||||
self.context
|
||||
.push(AssistantContext::Directory(DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
worktree,
|
||||
path,
|
||||
context_buffers,
|
||||
}));
|
||||
cx.notify();
|
||||
@@ -478,23 +479,31 @@ impl ContextStore {
|
||||
/// Returns whether the buffer is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory. Directory inclusion is based on paths rather than
|
||||
/// buffer IDs as the directory will be re-scanned.
|
||||
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
|
||||
pub fn will_include_buffer(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.files.get(&buffer_id) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
/// Returns whether this file path is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory.
|
||||
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
|
||||
pub fn will_include_file_path(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
cx: &App,
|
||||
) -> Option<FileInclusion> {
|
||||
if !self.files.is_empty() {
|
||||
let found_file_context = self.context.iter().find(|context| match &context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let buffer = file_context.context_buffer.buffer.read(cx);
|
||||
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
|
||||
*file_path == *path
|
||||
if let Some(context_path) = buffer.project_path(cx) {
|
||||
&context_path == project_path
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -506,31 +515,40 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
fn will_include_file_path_via_directory(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<FileInclusion> {
|
||||
if self.directories.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buf = path.to_path_buf();
|
||||
let mut path_buf = project_path.path.to_path_buf();
|
||||
|
||||
while buf.pop() {
|
||||
if let Some(_) = self.directories.get(&buf) {
|
||||
return Some(FileInclusion::InDirectory(buf));
|
||||
while path_buf.pop() {
|
||||
// TODO: This isn't very efficient. Consider using a better representation of the
|
||||
// directories map.
|
||||
let directory_project_path = ProjectPath {
|
||||
worktree_id: project_path.worktree_id,
|
||||
path: path_buf.clone().into(),
|
||||
};
|
||||
if let Some(_) = self.directories.get(&directory_project_path) {
|
||||
return Some(FileInclusion::InDirectory(directory_project_path));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(path) {
|
||||
pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(project_path) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
|
||||
@@ -564,13 +582,13 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
|
||||
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
|
||||
self.context
|
||||
.iter()
|
||||
.filter_map(|context| match context {
|
||||
AssistantContext::File(file) => {
|
||||
let buffer = file.context_buffer.buffer.read(cx);
|
||||
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
|
||||
buffer.project_path(cx)
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
@@ -587,7 +605,7 @@ impl ContextStore {
|
||||
|
||||
pub enum FileInclusion {
|
||||
Direct(ContextId),
|
||||
InDirectory(PathBuf),
|
||||
InDirectory(ProjectPath),
|
||||
}
|
||||
|
||||
// ContextBuffer without text.
|
||||
@@ -654,19 +672,6 @@ fn collect_buffer_info_and_text(
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
|
||||
if let Some(file) = buffer.file() {
|
||||
let mut path = file.path().clone();
|
||||
if path.as_os_str().is_empty() {
|
||||
path = file.full_path(cx).into();
|
||||
}
|
||||
Some(path)
|
||||
} else {
|
||||
log::error!("Buffer that had a path unexpectedly no longer has a path.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
let path_extension = path.extension().and_then(|ext| ext.to_str());
|
||||
let path_string = path.to_string_lossy();
|
||||
@@ -742,13 +747,13 @@ pub fn refresh_context_store_text(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let directory_path = directory_context.project_path(cx);
|
||||
let should_refresh = changed_buffers.is_empty()
|
||||
|| changed_buffers.iter().any(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
|
||||
path.starts_with(&directory_context.project_path.path)
|
||||
})
|
||||
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
|
||||
return false;
|
||||
};
|
||||
buffer_path.starts_with(&directory_path)
|
||||
});
|
||||
|
||||
if should_refresh {
|
||||
@@ -835,14 +840,16 @@ fn refresh_directory_text(
|
||||
let context_buffers = future::join_all(futures);
|
||||
|
||||
let id = directory_context.id;
|
||||
let project_path = directory_context.project_path.clone();
|
||||
let worktree = directory_context.worktree.clone();
|
||||
let path = directory_context.path.clone();
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_buffers = context_buffers.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_directory_context = DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
worktree,
|
||||
path,
|
||||
context_buffers,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Directory(new_directory_context));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashSet;
|
||||
@@ -9,6 +10,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
@@ -93,26 +95,23 @@ impl ContextStrip {
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.full_path(cx);
|
||||
let project_path = active_buffer.project_path(cx)?;
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), &path)
|
||||
.will_include_buffer(active_buffer.remote_id(), &project_path)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
let file_name = active_buffer.file()?.file_name(cx);
|
||||
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
name: file_name.to_string_lossy().into_owned().into(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
icon_path,
|
||||
})
|
||||
|
||||
@@ -201,7 +201,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn is_editor_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).text(cx).is_empty()
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
|
||||
fn is_model_selected(&self, cx: &App) -> bool {
|
||||
|
||||
@@ -9,6 +9,7 @@ use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
@@ -677,6 +678,9 @@ impl Thread {
|
||||
git_checkpoint,
|
||||
});
|
||||
}
|
||||
|
||||
self.auto_capture_telemetry(cx);
|
||||
|
||||
message_id
|
||||
}
|
||||
|
||||
@@ -1000,20 +1004,6 @@ impl Thread {
|
||||
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
// Add reminder to the last user message about code blocks
|
||||
if let Some(last_user_message) = request
|
||||
.messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|msg| msg.role == Role::User)
|
||||
{
|
||||
last_user_message
|
||||
.content
|
||||
.push(MessageContent::Text(system_prompt_reminder(
|
||||
&self.prompt_builder,
|
||||
)));
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
@@ -1169,6 +1159,8 @@ impl Thread {
|
||||
thread.touch_updated_at();
|
||||
cx.emit(ThreadEvent::StreamedCompletion);
|
||||
cx.notify();
|
||||
|
||||
thread.auto_capture_telemetry(cx);
|
||||
})?;
|
||||
|
||||
smol::future::yield_now().await;
|
||||
@@ -1195,7 +1187,8 @@ impl Thread {
|
||||
match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
cx.emit(ThreadEvent::UsePendingTools);
|
||||
let tool_uses = thread.use_pending_tools(cx);
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
@@ -1224,6 +1217,8 @@ impl Thread {
|
||||
}
|
||||
cx.emit(ThreadEvent::DoneStreaming);
|
||||
|
||||
thread.auto_capture_telemetry(cx);
|
||||
|
||||
if let Ok(initial_usage) = initial_token_usage {
|
||||
let usage = thread.cumulative_token_usage.clone() - initial_usage;
|
||||
|
||||
@@ -1383,10 +1378,8 @@ impl Thread {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn use_pending_tools(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoIterator<Item = PendingToolUse> + use<> {
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
@@ -1474,18 +1467,36 @@ impl Thread {
|
||||
output,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
canceled: false,
|
||||
});
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn tool_finished(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
pending_tool_use: Option<PendingToolUse>,
|
||||
canceled: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
self.attach_tool_results(cx);
|
||||
if !canceled {
|
||||
self.send_to_model(model, RequestKind::Chat, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
|
||||
// Insert a user message to contain the tool results.
|
||||
self.insert_user_message(
|
||||
@@ -1509,11 +1520,12 @@ impl Thread {
|
||||
let mut canceled = false;
|
||||
for pending_tool_use in self.tool_use.cancel_pending() {
|
||||
canceled = true;
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id: pending_tool_use.id.clone(),
|
||||
pending_tool_use: Some(pending_tool_use),
|
||||
canceled: true,
|
||||
});
|
||||
self.tool_finished(
|
||||
pending_tool_use.id.clone(),
|
||||
Some(pending_tool_use),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
canceled
|
||||
};
|
||||
@@ -1544,6 +1556,13 @@ impl Thread {
|
||||
let thread_id = self.id().clone();
|
||||
let client = self.project.read(cx).client();
|
||||
|
||||
let enabled_tool_names: Vec<String> = self
|
||||
.tools()
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.map(|tool| tool.name().to_string())
|
||||
.collect();
|
||||
|
||||
self.message_feedback.insert(message_id, feedback);
|
||||
|
||||
cx.notify();
|
||||
@@ -1567,6 +1586,7 @@ impl Thread {
|
||||
"Assistant Thread Rated",
|
||||
rating,
|
||||
thread_id,
|
||||
enabled_tool_names,
|
||||
message_id = message_id.0,
|
||||
message_content,
|
||||
thread_data,
|
||||
@@ -1839,6 +1859,65 @@ impl Thread {
|
||||
self.cumulative_token_usage.clone()
|
||||
}
|
||||
|
||||
pub fn auto_capture_telemetry(&self, cx: &mut Context<Self>) {
|
||||
static mut LAST_CAPTURE: Option<std::time::Instant> = None;
|
||||
let now = std::time::Instant::now();
|
||||
let should_check = unsafe {
|
||||
if let Some(last) = LAST_CAPTURE {
|
||||
if now.duration_since(last).as_secs() < 10 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
LAST_CAPTURE = Some(now);
|
||||
true
|
||||
};
|
||||
|
||||
if !should_check {
|
||||
return;
|
||||
}
|
||||
|
||||
let feature_flag_enabled = cx.has_flag::<feature_flags::ThreadAutoCapture>();
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
if !feature_flag_enabled {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let thread_id = self.id().clone();
|
||||
|
||||
let github_handle = self
|
||||
.project
|
||||
.read(cx)
|
||||
.user_store()
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|user| user.github_login.clone());
|
||||
|
||||
let client = self.project.read(cx).client().clone();
|
||||
|
||||
let serialized_thread = self.serialize(cx);
|
||||
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
if let Ok(serialized_thread) = serialized_thread.await {
|
||||
let thread_data = serde_json::to_value(serialized_thread)
|
||||
.unwrap_or_else(|_| serde_json::Value::Null);
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Thread AutoCaptured",
|
||||
thread_id = thread_id.to_string(),
|
||||
thread_data = thread_data,
|
||||
auto_capture_reason = "tracked_user",
|
||||
github_handle = github_handle
|
||||
);
|
||||
|
||||
client.telemetry().flush_events();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.default_model() else {
|
||||
@@ -1880,21 +1959,10 @@ impl Thread {
|
||||
|
||||
self.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use: None,
|
||||
canceled: true,
|
||||
});
|
||||
self.tool_finished(tool_use_id.clone(), None, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_prompt_reminder(prompt_builder: &prompt_store::PromptBuilder) -> String {
|
||||
prompt_builder
|
||||
.generate_assistant_system_prompt_reminder()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
@@ -1917,14 +1985,14 @@ pub enum ThreadEvent {
|
||||
MessageDeleted(MessageId),
|
||||
SummaryGenerated,
|
||||
SummaryChanged,
|
||||
UsePendingTools,
|
||||
UsePendingTools {
|
||||
tool_uses: Vec<PendingToolUse>,
|
||||
},
|
||||
ToolFinished {
|
||||
#[allow(unused)]
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
/// The pending tool use that corresponds to this tool.
|
||||
pending_tool_use: Option<PendingToolUse>,
|
||||
/// Whether the tool was canceled by the user.
|
||||
canceled: bool,
|
||||
},
|
||||
CheckpointChanged,
|
||||
ToolConfirmationNeeded,
|
||||
@@ -1964,7 +2032,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
add_file_to_context(&project, &context_store, "test/code.rs", cx)
|
||||
@@ -2018,14 +2086,8 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
let actual_message = request.messages[0].string_contents();
|
||||
let expected_content = format!(
|
||||
"{}Please explain this code{}",
|
||||
expected_context,
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
let expected_full_message = format!("{}Please explain this code", expected_context);
|
||||
assert_eq!(request.messages[0].string_contents(), expected_full_message);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2042,7 +2104,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, context_store, _prompt_builder) =
|
||||
let (_, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Open files individually
|
||||
@@ -2142,7 +2204,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, prompt_builder) =
|
||||
let (_, _thread_store, thread, _context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert user message without any context (empty context vector)
|
||||
@@ -2168,14 +2230,11 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
let actual_message = request.messages[0].string_contents();
|
||||
let expected_content = format!(
|
||||
"What is the best way to learn Rust?{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
assert_eq!(
|
||||
request.messages[0].string_contents(),
|
||||
"What is the best way to learn Rust?"
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
|
||||
// Add second message, also without context
|
||||
let message2_id = thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Are there any good books?", vec![], None, cx)
|
||||
@@ -2191,17 +2250,14 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
// First message should be the system prompt
|
||||
assert_eq!(request.messages[0].role, Role::User);
|
||||
|
||||
// Second message should be the user message with prompt reminder
|
||||
let actual_message = request.messages[1].string_contents();
|
||||
let expected_content = format!(
|
||||
"Are there any good books?{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
assert_eq!(
|
||||
request.messages[0].string_contents(),
|
||||
"What is the best way to learn Rust?"
|
||||
);
|
||||
assert_eq!(
|
||||
request.messages[1].string_contents(),
|
||||
"Are there any good books?"
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2214,7 +2270,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Open buffer and add it to context
|
||||
@@ -2274,14 +2330,11 @@ fn main() {{
|
||||
// The last message should be the stale buffer notification
|
||||
assert_eq!(last_message.role, Role::User);
|
||||
|
||||
let actual_message = last_message.string_contents();
|
||||
let expected_content = format!(
|
||||
"These files changed since last read:\n- code.rs\n{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
// Check the exact content of the message
|
||||
let expected_content = "These files changed since last read:\n- code.rs\n";
|
||||
assert_eq!(
|
||||
actual_message, expected_content,
|
||||
last_message.string_contents(),
|
||||
expected_content,
|
||||
"Last message should be exactly the stale buffer notification"
|
||||
);
|
||||
}
|
||||
@@ -2319,27 +2372,24 @@ fn main() {{
|
||||
Entity<ThreadStore>,
|
||||
Entity<Thread>,
|
||||
Entity<ContextStore>,
|
||||
Arc<PromptBuilder>,
|
||||
) {
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
|
||||
let thread_store = cx.update(|_, cx| {
|
||||
ThreadStore::new(project.clone(), Arc::default(), prompt_builder.clone(), cx).unwrap()
|
||||
ThreadStore::new(
|
||||
project.clone(),
|
||||
Arc::default(),
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
(
|
||||
workspace,
|
||||
thread_store,
|
||||
thread,
|
||||
context_store,
|
||||
prompt_builder,
|
||||
)
|
||||
(workspace, thread_store, thread, context_store)
|
||||
}
|
||||
|
||||
async fn add_file_to_context(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod agent_notification;
|
||||
mod context_pill;
|
||||
mod user_spending;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use context_pill::*;
|
||||
// pub use user_spending::*;
|
||||
|
||||
@@ -280,9 +280,10 @@ impl AddedContext {
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
|
||||
// handle renames?
|
||||
let full_path = &directory_context.project_path.path;
|
||||
let full_path = directory_context
|
||||
.worktree
|
||||
.read(cx)
|
||||
.full_path(&directory_context.path);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
|
||||
186
crates/agent/src/ui/user_spending.rs
Normal file
186
crates/agent/src/ui/user_spending.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use gpui::{Entity, Render};
|
||||
use ui::{ProgressBar, prelude::*};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct UserSpending {
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
free_tier_progress: Entity<ProgressBar>,
|
||||
over_tier_progress: Entity<ProgressBar>,
|
||||
}
|
||||
|
||||
impl UserSpending {
|
||||
pub fn new(
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let free_tier_capped = free_tier_current == free_tier_cap;
|
||||
let free_tier_near_capped =
|
||||
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
|
||||
let over_tier_capped = over_tier_current == over_tier_cap;
|
||||
let over_tier_near_capped =
|
||||
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
|
||||
|
||||
let free_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"free_tier",
|
||||
free_tier_current as f32,
|
||||
free_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let over_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"over_tier",
|
||||
over_tier_current as f32,
|
||||
over_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if free_tier_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if free_tier_near_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
if over_tier_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if over_tier_near_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
free_tier_current,
|
||||
free_tier_cap,
|
||||
over_tier_current,
|
||||
over_tier_cap,
|
||||
free_tier_progress,
|
||||
over_tier_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for UserSpending {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let formatted_free_tier = format!(
|
||||
"${} / ${}",
|
||||
self.free_tier_current as f32 / 100.0,
|
||||
self.free_tier_cap as f32 / 100.0
|
||||
);
|
||||
let formatted_over_tier = format!(
|
||||
"${} / ${}",
|
||||
self.over_tier_current as f32 / 100.0,
|
||||
self.over_tier_cap as f32 / 100.0
|
||||
);
|
||||
|
||||
v_group()
|
||||
.elevation_2(cx)
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.w(px(360.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_free_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.free_tier_progress.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Current Spending").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_over_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.over_tier_progress.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UserSpending {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
|
||||
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
|
||||
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
|
||||
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
|
||||
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"New User",
|
||||
div().size_full().child(new_user.clone()).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,11 +95,7 @@ impl HeadlessAssistant {
|
||||
self.done_tx.send_blocking(Ok(())).unwrap()
|
||||
}
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.use_pending_tools(cx);
|
||||
});
|
||||
}
|
||||
ThreadEvent::UsePendingTools { .. } => {}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
// Automatically approve all tools that need confirmation in headless mode
|
||||
println!("Tool confirmation needed - automatically approving in headless mode");
|
||||
@@ -152,19 +148,6 @@ impl HeadlessAssistant {
|
||||
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
|
||||
println!("Tool result: {:?}", tool_result);
|
||||
}
|
||||
if thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.default_model() {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.attach_tool_results(cx);
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx);
|
||||
});
|
||||
} else {
|
||||
println!(
|
||||
"Warning: No active language model available to continue conversation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod bash_tool;
|
||||
mod batch_tool;
|
||||
mod code_action_tool;
|
||||
mod code_symbols_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
@@ -15,9 +15,11 @@ mod open_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod rename_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -28,8 +30,8 @@ use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_action_tool::CodeActionTool;
|
||||
use crate::code_symbols_tool::CodeSymbolsTool;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
@@ -43,14 +45,16 @@ use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search_tool::RegexSearchTool;
|
||||
use crate::rename_tool::RenameTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(BatchTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
@@ -58,6 +62,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
@@ -67,6 +72,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Executes a bash one-liner and returns the combined output.
|
||||
|
||||
This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
|
||||
|
||||
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.
|
||||
389
crates/assistant_tools/src/code_action_tool.rs
Normal file
389
crates/assistant_tools/src/code_action_tool.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, ToPointUtf16};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{self, LspAction, Project};
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeActionToolInput {
|
||||
/// The relative path to the file containing the text range.
|
||||
///
|
||||
/// WARNING: you MUST start this path with one of the project's root directories.
|
||||
pub path: String,
|
||||
|
||||
/// The specific code action to execute.
|
||||
///
|
||||
/// If this field is provided, the tool will execute the specified action.
|
||||
/// If omitted, the tool will list all available code actions for the text range.
|
||||
///
|
||||
/// Here are some actions that are commonly supported (but may not be for this particular
|
||||
/// text range; you can omit this field to list all the actions, if you want to know
|
||||
/// what your options are, or you can just try an action and if it fails I'll tell you
|
||||
/// what the available actions were instead):
|
||||
/// - "quickfix.all" - applies all available quick fixes in the range
|
||||
/// - "source.organizeImports" - sorts and cleans up import statements
|
||||
/// - "source.fixAll" - applies all available auto fixes
|
||||
/// - "refactor.extract" - extracts selected code into a new function or variable
|
||||
/// - "refactor.inline" - inlines a variable by replacing references with its value
|
||||
/// - "refactor.rewrite" - general code rewriting operations
|
||||
/// - "source.addMissingImports" - adds imports for references that lack them
|
||||
/// - "source.removeUnusedImports" - removes imports that aren't being used
|
||||
/// - "source.implementInterface" - generates methods required by an interface/trait
|
||||
/// - "source.generateAccessors" - creates getter/setter methods
|
||||
/// - "source.convertToAsyncFunction" - converts callback-style code to async/await
|
||||
///
|
||||
/// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
|
||||
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
|
||||
pub action: Option<String>,
|
||||
|
||||
/// Optional arguments to pass to the code action.
|
||||
///
|
||||
/// For rename operations (when action="textDocument/rename"), this should contain the new name.
|
||||
/// For other code actions, these arguments may be passed to the language server.
|
||||
pub arguments: Option<serde_json::Value>,
|
||||
|
||||
/// The text that comes immediately before the text range in the file.
|
||||
pub context_before_range: String,
|
||||
|
||||
/// The text range. This text must appear in the file right between `context_before_range`
|
||||
/// and `context_after_range`.
|
||||
///
|
||||
/// The file must contain exactly one occurrence of `context_before_range` followed by
|
||||
/// `text_range` followed by `context_after_range`. If the file contains zero occurrences,
|
||||
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
|
||||
/// critical that you verify ahead of time that the string is unique. You can search
|
||||
/// the file's contents to verify this ahead of time.
|
||||
///
|
||||
/// To make the string more likely to be unique, include a minimum of 1 line of context
|
||||
/// before the text range, as well as a minimum of 1 line of context after the text range.
|
||||
/// If these lines of context are not enough to obtain a string that appears only once
|
||||
/// in the file, then double the number of context lines until the string becomes unique.
|
||||
/// (Start with 1 line before and 1 line after though, because too much context is
|
||||
/// needlessly costly.)
|
||||
///
|
||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
||||
/// whitespace and indentation for all lines of code. The combined string must be exactly
|
||||
/// as it appears in the file, or else this tool call will fail.
|
||||
pub text_range: String,
|
||||
|
||||
/// The text that comes immediately after the text range in the file.
|
||||
pub context_after_range: String,
|
||||
}
|
||||
|
||||
pub struct CodeActionTool;
|
||||
|
||||
impl Tool for CodeActionTool {
|
||||
fn name(&self) -> String {
|
||||
"code_actions".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./code_action_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Wand
|
||||
}
|
||||
|
||||
fn input_schema(
|
||||
&self,
|
||||
_format: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CodeActionToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CodeActionToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
if let Some(action) = &input.action {
|
||||
if action == "textDocument/rename" {
|
||||
let new_name = match &input.arguments {
|
||||
Some(serde_json::Value::String(new_name)) => new_name.clone(),
|
||||
Some(value) => {
|
||||
if let Ok(new_name) =
|
||||
serde_json::from_value::<String>(value.clone())
|
||||
{
|
||||
new_name
|
||||
} else {
|
||||
"invalid name".to_string()
|
||||
}
|
||||
}
|
||||
None => "missing name".to_string(),
|
||||
};
|
||||
format!("Rename '{}' to '{}'", input.text_range, new_name)
|
||||
} else {
|
||||
format!(
|
||||
"Execute code action '{}' for '{}'",
|
||||
action, input.text_range
|
||||
)
|
||||
}
|
||||
} else {
|
||||
format!("List available code actions for '{}'", input.text_range)
|
||||
}
|
||||
}
|
||||
Err(_) => "Perform code action".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let range = {
|
||||
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
|
||||
find_text_range(&buffer, &input.context_before_range, &input.text_range, &input.context_after_range)
|
||||
})? else {
|
||||
return Err(anyhow!(
|
||||
"Failed to locate the text specified by context_before_range, text_range, and context_after_range. Make sure context_before_range and context_after_range each match exactly once in the file."
|
||||
));
|
||||
};
|
||||
|
||||
range
|
||||
};
|
||||
|
||||
if let Some(action_type) = &input.action {
|
||||
// Special-case the `rename` operation
|
||||
let response = if action_type == "textDocument/rename" {
|
||||
let Some(new_name) = input.arguments.and_then(|args| serde_json::from_value::<String>(args).ok()) else {
|
||||
return Err(anyhow!("For rename operations, 'arguments' must be a string containing the new name"));
|
||||
};
|
||||
|
||||
let position = buffer.read_with(cx, |buffer, _| {
|
||||
range.start.to_point_utf16(&buffer.snapshot())
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.perform_rename(buffer.clone(), position, new_name.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
format!("Renamed '{}' to '{}'", input.text_range, new_name)
|
||||
} else {
|
||||
// Get code actions for the range
|
||||
let actions = project
|
||||
.update(cx, |project, cx| {
|
||||
project.code_actions(&buffer, range.clone(), None, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if actions.is_empty() {
|
||||
return Err(anyhow!("No code actions available for this range"));
|
||||
}
|
||||
|
||||
// Find all matching actions
|
||||
let regex = match Regex::new(action_type) {
|
||||
Ok(regex) => regex,
|
||||
Err(err) => return Err(anyhow!("Invalid regex pattern: {}", err)),
|
||||
};
|
||||
let mut matching_actions = actions
|
||||
.into_iter()
|
||||
.filter(|action| { regex.is_match(action.lsp_action.title()) });
|
||||
|
||||
let Some(action) = matching_actions.next() else {
|
||||
return Err(anyhow!("No code actions match the pattern: {}", action_type));
|
||||
};
|
||||
|
||||
// There should have been exactly one matching action.
|
||||
if let Some(second) = matching_actions.next() {
|
||||
let mut all_matches = vec![action, second];
|
||||
|
||||
all_matches.extend(matching_actions);
|
||||
|
||||
return Err(anyhow!(
|
||||
"Pattern '{}' matches multiple code actions: {}",
|
||||
action_type,
|
||||
all_matches.into_iter().map(|action| action.lsp_action.title().to_string()).collect::<Vec<_>>().join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
let title = action.lsp_action.title().to_string();
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer.clone(), action, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
format!("Completed code action: {}", title)
|
||||
};
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
} else {
|
||||
// No action specified, so list the available ones.
|
||||
let (position_start, position_end) = buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
(
|
||||
range.start.to_point_utf16(&snapshot),
|
||||
range.end.to_point_utf16(&snapshot)
|
||||
)
|
||||
})?;
|
||||
|
||||
// Convert position to display coordinates (1-based)
|
||||
let position_start_display = language::Point {
|
||||
row: position_start.row + 1,
|
||||
column: position_start.column + 1,
|
||||
};
|
||||
|
||||
let position_end_display = language::Point {
|
||||
row: position_end.row + 1,
|
||||
column: position_end.column + 1,
|
||||
};
|
||||
|
||||
// Get code actions for the range
|
||||
let actions = project
|
||||
.update(cx, |project, cx| {
|
||||
project.code_actions(&buffer, range.clone(), None, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let mut response = format!(
|
||||
"Available code actions for text range '{}' at position {}:{} to {}:{} (UTF-16 coordinates):\n\n",
|
||||
input.text_range,
|
||||
position_start_display.row, position_start_display.column,
|
||||
position_end_display.row, position_end_display.column
|
||||
);
|
||||
|
||||
if actions.is_empty() {
|
||||
response.push_str("No code actions available for this range.");
|
||||
} else {
|
||||
for (i, action) in actions.iter().enumerate() {
|
||||
let title = match &action.lsp_action {
|
||||
LspAction::Action(code_action) => code_action.title.as_str(),
|
||||
LspAction::Command(command) => command.title.as_str(),
|
||||
LspAction::CodeLens(code_lens) => {
|
||||
if let Some(cmd) = &code_lens.command {
|
||||
cmd.title.as_str()
|
||||
} else {
|
||||
"Unknown code lens"
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let kind = match &action.lsp_action {
|
||||
LspAction::Action(code_action) => {
|
||||
if let Some(kind) = &code_action.kind {
|
||||
kind.as_str()
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
},
|
||||
LspAction::Command(_) => "command",
|
||||
LspAction::CodeLens(_) => "code_lens",
|
||||
};
|
||||
|
||||
response.push_str(&format!("{}. {title} ({kind})\n", i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the range of the text in the buffer, if it appears between context_before_range
|
||||
/// and context_after_range, and if that combined string has one unique result in the buffer.
|
||||
///
|
||||
/// If an exact match fails, it tries adding a newline to the end of context_before_range and
|
||||
/// to the beginning of context_after_range to accommodate line-based context matching.
|
||||
fn find_text_range(
|
||||
buffer: &Buffer,
|
||||
context_before_range: &str,
|
||||
text_range: &str,
|
||||
context_after_range: &str,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let snapshot = buffer.snapshot();
|
||||
let text = snapshot.text();
|
||||
|
||||
// First try with exact match
|
||||
let search_string = format!("{context_before_range}{text_range}{context_after_range}");
|
||||
let mut positions = text.match_indices(&search_string);
|
||||
let position_result = positions.next();
|
||||
|
||||
if let Some(position) = position_result {
|
||||
// Check if the matched string is unique
|
||||
if positions.next().is_none() {
|
||||
let range_start = position.0 + context_before_range.len();
|
||||
let range_end = range_start + text_range.len();
|
||||
let range_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_start));
|
||||
let range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_end));
|
||||
|
||||
return Some(range_start_anchor..range_end_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
// If exact match fails or is not unique, try with line-based context
|
||||
// Add a newline to the end of before context and beginning of after context
|
||||
let line_based_before = if context_before_range.ends_with('\n') {
|
||||
context_before_range.to_string()
|
||||
} else {
|
||||
format!("{context_before_range}\n")
|
||||
};
|
||||
|
||||
let line_based_after = if context_after_range.starts_with('\n') {
|
||||
context_after_range.to_string()
|
||||
} else {
|
||||
format!("\n{context_after_range}")
|
||||
};
|
||||
|
||||
let line_search_string = format!("{line_based_before}{text_range}{line_based_after}");
|
||||
let mut line_positions = text.match_indices(&line_search_string);
|
||||
let line_position = line_positions.next()?;
|
||||
|
||||
// The line-based search string must also appear exactly once
|
||||
if line_positions.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line_range_start = line_position.0 + line_based_before.len();
|
||||
let line_range_end = line_range_start + text_range.len();
|
||||
let line_range_start_anchor =
|
||||
snapshot.anchor_before(snapshot.offset_to_point(line_range_start));
|
||||
let line_range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(line_range_end));
|
||||
|
||||
Some(line_range_start_anchor..line_range_end_anchor)
|
||||
}
|
||||
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
@@ -0,0 +1,19 @@
|
||||
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
|
||||
|
||||
This tool can:
|
||||
- List all available code actions for a selected text range
|
||||
- Execute a specific code action on that range
|
||||
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
|
||||
|
||||
Use this tool when you want to:
|
||||
- Discover what code actions are available for a piece of code
|
||||
- Apply automatic fixes and code transformations
|
||||
- Rename variables, functions, or other symbols consistently throughout your project
|
||||
- Clean up imports, implement interfaces, or perform other language-specific operations
|
||||
|
||||
- If unsure what actions are available, call the tool without specifying an action to get a list
|
||||
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
|
||||
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
|
||||
- Be specific with your text range and context to ensure the tool identifies the correct code location
|
||||
|
||||
The tool will automatically save any changes it makes to your files.
|
||||
@@ -63,6 +63,16 @@ pub struct FindReplaceFileToolInput {
|
||||
/// even one character in this string is different in any way from how it appears
|
||||
/// in the file, then the tool call will fail.
|
||||
///
|
||||
/// If you get an error that the `find` string was not found, this means that either
|
||||
/// you made a mistake, or that the file has changed since you last looked at it.
|
||||
/// Either way, when this happens, you should retry doing this tool call until it
|
||||
/// succeeds, up to 3 times. Each time you retry, you should take another look at
|
||||
/// the exact text of the file in question, to make sure that you are searching for
|
||||
/// exactly the right string. Regardless of whether it was because you made a mistake
|
||||
/// or because the file changed since you last looked at it, you should be extra
|
||||
/// careful when retrying in this way. It's a bad experience for the user if
|
||||
/// this `find` string isn't found, so be super careful to get it exactly right!
|
||||
///
|
||||
/// <example>
|
||||
/// If a file contains this code:
|
||||
///
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
Find one unique part of a file in the project and replace that text with new text.
|
||||
|
||||
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
|
||||
This tool is the preferred way to make edits to files *except* when making a rename. When making a rename specifically, the rename tool must always be used instead.
|
||||
|
||||
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing a batch of calls to this tool - one call for each find/replace operation.
|
||||
|
||||
You should only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
|
||||
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ impl Tool for RegexSearchTool {
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
PathMatcher::default(),
|
||||
PathMatcher::default(),
|
||||
None,
|
||||
|
||||
205
crates/assistant_tools/src/rename_tool.rs
Normal file
205
crates/assistant_tools/src/rename_tool.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Buffer, ToPointUtf16};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RenameToolInput {
|
||||
/// The relative path to the file containing the symbol to rename.
|
||||
///
|
||||
/// WARNING: you MUST start this path with one of the project's root directories.
|
||||
pub path: String,
|
||||
|
||||
/// The new name to give to the symbol.
|
||||
pub new_name: String,
|
||||
|
||||
/// The text that comes immediately before the symbol in the file.
|
||||
pub context_before_symbol: String,
|
||||
|
||||
/// The symbol to rename. This text must appear in the file right between
|
||||
/// `context_before_symbol` and `context_after_symbol`.
|
||||
///
|
||||
/// The file must contain exactly one occurrence of `context_before_symbol` followed by
|
||||
/// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
|
||||
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
|
||||
/// critical that you verify ahead of time that the string is unique. You can search
|
||||
/// the file's contents to verify this ahead of time.
|
||||
///
|
||||
/// To make the string more likely to be unique, include a minimum of 1 line of context
|
||||
/// before the symbol, as well as a minimum of 1 line of context after the symbol.
|
||||
/// If these lines of context are not enough to obtain a string that appears only once
|
||||
/// in the file, then double the number of context lines until the string becomes unique.
|
||||
/// (Start with 1 line before and 1 line after though, because too much context is
|
||||
/// needlessly costly.)
|
||||
///
|
||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
||||
/// whitespace and indentation for all lines of code. The combined string must be exactly
|
||||
/// as it appears in the file, or else this tool call will fail.
|
||||
pub symbol: String,
|
||||
|
||||
/// The text that comes immediately after the symbol in the file.
|
||||
pub context_after_symbol: String,
|
||||
}
|
||||
|
||||
pub struct RenameTool;
|
||||
|
||||
impl Tool for RenameTool {
|
||||
fn name(&self) -> String {
|
||||
"rename".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./rename_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(
|
||||
&self,
|
||||
_format: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(RenameToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<RenameToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
format!("Rename '{}' to '{}'", input.symbol, input.new_name)
|
||||
}
|
||||
Err(_) => "Rename symbol".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<RenameToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let position = {
|
||||
let Some(position) = buffer.read_with(cx, |buffer, _cx| {
|
||||
find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
|
||||
})? else {
|
||||
return Err(anyhow!(
|
||||
"Failed to locate the symbol specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
|
||||
));
|
||||
};
|
||||
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
position.to_point_utf16(&buffer.snapshot())
|
||||
})?
|
||||
};
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
})?;
|
||||
|
||||
Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the position of the symbol in the buffer, if it appears between context_before_symbol
|
||||
/// and context_after_symbol, and if that combined string has one unique result in the buffer.
|
||||
///
|
||||
/// If an exact match fails, it tries adding a newline to the end of context_before_symbol and
|
||||
/// to the beginning of context_after_symbol to accommodate line-based context matching.
|
||||
fn find_symbol_position(
|
||||
buffer: &Buffer,
|
||||
context_before_symbol: &str,
|
||||
symbol: &str,
|
||||
context_after_symbol: &str,
|
||||
) -> Option<language::Anchor> {
|
||||
let snapshot = buffer.snapshot();
|
||||
let text = snapshot.text();
|
||||
|
||||
// First try with exact match
|
||||
let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
|
||||
let mut positions = text.match_indices(&search_string);
|
||||
let position_result = positions.next();
|
||||
|
||||
if let Some(position) = position_result {
|
||||
// Check if the matched string is unique
|
||||
if positions.next().is_none() {
|
||||
let symbol_start = position.0 + context_before_symbol.len();
|
||||
let symbol_start_anchor =
|
||||
snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
|
||||
|
||||
return Some(symbol_start_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
// If exact match fails or is not unique, try with line-based context
|
||||
// Add a newline to the end of before context and beginning of after context
|
||||
let line_based_before = if context_before_symbol.ends_with('\n') {
|
||||
context_before_symbol.to_string()
|
||||
} else {
|
||||
format!("{context_before_symbol}\n")
|
||||
};
|
||||
|
||||
let line_based_after = if context_after_symbol.starts_with('\n') {
|
||||
context_after_symbol.to_string()
|
||||
} else {
|
||||
format!("\n{context_after_symbol}")
|
||||
};
|
||||
|
||||
let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");
|
||||
let mut line_positions = text.match_indices(&line_search_string);
|
||||
let line_position = line_positions.next()?;
|
||||
|
||||
// The line-based search string must also appear exactly once
|
||||
if line_positions.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line_symbol_start = line_position.0 + line_based_before.len();
|
||||
let line_symbol_start_anchor =
|
||||
snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));
|
||||
|
||||
Some(line_symbol_start_anchor)
|
||||
}
|
||||
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Renames a symbol across your codebase using the language server's semantic knowledge.
|
||||
|
||||
This tool performs a rename refactoring operation on a specified symbol. It uses the project's language server to analyze the code and perform the rename correctly across all files where the symbol is referenced.
|
||||
|
||||
Unlike a simple find and replace, this tool understands the semantic meaning of the code, so it only renames the specific symbol you specify and not unrelated text that happens to have the same name.
|
||||
|
||||
Examples of symbols you can rename:
|
||||
- Variables
|
||||
- Functions
|
||||
- Classes/structs
|
||||
- Fields/properties
|
||||
- Methods
|
||||
- Interfaces/traits
|
||||
|
||||
The language server handles updating all references to the renamed symbol throughout the codebase.
|
||||
@@ -2,12 +2,15 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future;
|
||||
use util::get_system_shell;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
@@ -15,18 +18,18 @@ use util::command::new_smol_command;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
/// The bash one-liner command to execute.
|
||||
pub struct TerminalToolInput {
|
||||
/// The one-liner command to execute.
|
||||
command: String,
|
||||
/// Working directory for the command. This must be one of the root directories of the project.
|
||||
cd: String,
|
||||
}
|
||||
|
||||
pub struct BashTool;
|
||||
pub struct TerminalTool;
|
||||
|
||||
impl Tool for BashTool {
|
||||
impl Tool for TerminalTool {
|
||||
fn name(&self) -> String {
|
||||
"bash".to_string()
|
||||
"terminal".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
@@ -34,7 +37,7 @@ impl Tool for BashTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./bash_tool/description.md").to_string()
|
||||
include_str!("./terminal_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -42,11 +45,11 @@ impl Tool for BashTool {
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<BashToolInput>(format)
|
||||
json_schema_for::<TerminalToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<BashToolInput>(input.clone()) {
|
||||
match serde_json::from_value::<TerminalToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let mut lines = input.command.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
@@ -65,7 +68,7 @@ impl Tool for BashTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => "Run bash command".to_string(),
|
||||
Err(_) => "Run terminal command".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +80,7 @@ impl Tool for BashTool {
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: BashToolInput = match serde_json::from_value(input) {
|
||||
let input: TerminalToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
@@ -130,67 +133,87 @@ impl Tool for BashTool {
|
||||
const LIMIT: usize = 16 * 1024;
|
||||
|
||||
async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<String> {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving.
|
||||
let command = format!("({}) 2>&1", command);
|
||||
let shell = get_system_shell();
|
||||
|
||||
let mut cmd = new_smol_command("bash")
|
||||
let mut cmd = new_smol_command(&shell)
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to execute bash command")?;
|
||||
.context("Failed to execute terminal command")?;
|
||||
|
||||
// Capture stdout with a limit
|
||||
let stdout = cmd.stdout.take().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
let mut combined_buffer = String::with_capacity(LIMIT + 1);
|
||||
|
||||
// Read one more byte to determine whether the output was truncated
|
||||
let mut buffer = vec![0; LIMIT + 1];
|
||||
let mut bytes_read = 0;
|
||||
let mut out_reader = BufReader::new(cmd.stdout.take().context("Failed to get stdout")?);
|
||||
let mut out_tmp_buffer = String::with_capacity(512);
|
||||
let mut err_reader = BufReader::new(cmd.stderr.take().context("Failed to get stderr")?);
|
||||
let mut err_tmp_buffer = String::with_capacity(512);
|
||||
|
||||
// Read until we reach the limit
|
||||
loop {
|
||||
let read = reader.read(&mut buffer[bytes_read..]).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
let mut out_line = Box::pin(
|
||||
out_reader
|
||||
.read_line(&mut out_tmp_buffer)
|
||||
.left_future()
|
||||
.fuse(),
|
||||
);
|
||||
let mut err_line = Box::pin(
|
||||
err_reader
|
||||
.read_line(&mut err_tmp_buffer)
|
||||
.left_future()
|
||||
.fuse(),
|
||||
);
|
||||
|
||||
bytes_read += read;
|
||||
if bytes_read > LIMIT {
|
||||
bytes_read = LIMIT + 1;
|
||||
break;
|
||||
}
|
||||
let mut has_stdout = true;
|
||||
let mut has_stderr = true;
|
||||
while (has_stdout || has_stderr) && combined_buffer.len() < LIMIT + 1 {
|
||||
futures::select_biased! {
|
||||
read = out_line => {
|
||||
drop(out_line);
|
||||
combined_buffer.extend(out_tmp_buffer.drain(..));
|
||||
if read? == 0 {
|
||||
out_line = Box::pin(future::pending().right_future().fuse());
|
||||
has_stdout = false;
|
||||
} else {
|
||||
out_line = Box::pin(out_reader.read_line(&mut out_tmp_buffer).left_future().fuse());
|
||||
}
|
||||
}
|
||||
read = err_line => {
|
||||
drop(err_line);
|
||||
combined_buffer.extend(err_tmp_buffer.drain(..));
|
||||
if read? == 0 {
|
||||
err_line = Box::pin(future::pending().right_future().fuse());
|
||||
has_stderr = false;
|
||||
} else {
|
||||
err_line = Box::pin(err_reader.read_line(&mut err_tmp_buffer).left_future().fuse());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Repeatedly fill the output reader's buffer without copying it.
|
||||
loop {
|
||||
let skipped_bytes = reader.fill_buf().await?;
|
||||
if skipped_bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
let skipped_bytes_len = skipped_bytes.len();
|
||||
reader.consume_unpin(skipped_bytes_len);
|
||||
}
|
||||
drop((out_line, err_line));
|
||||
|
||||
let output_bytes = &buffer[..bytes_read.min(LIMIT)];
|
||||
let truncated = combined_buffer.len() > LIMIT;
|
||||
combined_buffer.truncate(LIMIT);
|
||||
|
||||
consume_reader(out_reader, truncated).await?;
|
||||
consume_reader(err_reader, truncated).await?;
|
||||
|
||||
let status = cmd.status().await.context("Failed to get command status")?;
|
||||
|
||||
let output_string = if bytes_read > LIMIT {
|
||||
let output_string = if truncated {
|
||||
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
|
||||
// multi-byte characters.
|
||||
let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');
|
||||
let until_last_line = &output_bytes[..last_line_ix.unwrap_or(output_bytes.len())];
|
||||
let output_string = String::from_utf8_lossy(until_last_line);
|
||||
let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n');
|
||||
let combined_buffer = &combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())];
|
||||
|
||||
format!(
|
||||
"Command output too long. The first {} bytes:\n\n{}",
|
||||
output_string.len(),
|
||||
output_block(&output_string),
|
||||
combined_buffer.len(),
|
||||
output_block(&combined_buffer),
|
||||
)
|
||||
} else {
|
||||
output_block(&String::from_utf8_lossy(&output_bytes))
|
||||
output_block(&combined_buffer)
|
||||
};
|
||||
|
||||
let output_with_status = if status.success() {
|
||||
@@ -201,8 +224,9 @@ async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"Command failed with exit code {}\n\n{}",
|
||||
"Command failed with exit code {} (shell: {}).\n\n{}",
|
||||
status.code().unwrap_or(-1),
|
||||
shell,
|
||||
output_string,
|
||||
)
|
||||
};
|
||||
@@ -210,6 +234,24 @@ async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<
|
||||
Ok(output_with_status)
|
||||
}
|
||||
|
||||
async fn consume_reader<T: AsyncReadExt + Unpin>(
|
||||
mut reader: BufReader<T>,
|
||||
truncated: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
loop {
|
||||
let skipped_bytes = reader.fill_buf().await?;
|
||||
if skipped_bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
let skipped_bytes_len = skipped_bytes.len();
|
||||
reader.consume_unpin(skipped_bytes_len);
|
||||
|
||||
// Should only skip if we went over the limit
|
||||
debug_assert!(truncated);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_block(output: &str) -> String {
|
||||
format!(
|
||||
"```\n{}{}```",
|
||||
@@ -225,7 +267,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_run_command_simple(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
@@ -236,12 +278,11 @@ mod tests {
|
||||
assert_eq!(result.unwrap(), "```\nHello, World!\n```");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let command =
|
||||
"echo 'stdout 1' && echo 'stderr 1' >&2 && echo 'stdout 2' && echo 'stderr 2' >&2";
|
||||
let command = "echo 'stdout 1' && sleep 0.01 && echo 'stderr 1' >&2 && sleep 0.01 && echo 'stdout 2' && sleep 0.01 && echo 'stderr 2' >&2";
|
||||
let result = run_command_limited(Path::new(".").into(), command.to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
@@ -251,7 +292,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_multiple_output_reads(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
@@ -266,11 +307,11 @@ mod tests {
|
||||
assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let cmd = format!("echo '{}';", "X".repeat(LIMIT * 2));
|
||||
let cmd = format!("echo '{}'; sleep 0.01;", "X".repeat(LIMIT * 2));
|
||||
|
||||
let result = run_command_limited(Path::new(".").into(), cmd).await;
|
||||
|
||||
@@ -285,7 +326,7 @@ mod tests {
|
||||
assert_eq!(content_length, LIMIT);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
@@ -303,4 +344,23 @@ mod tests {
|
||||
|
||||
assert!(content_length <= LIMIT);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_command_failure(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let result = run_command_limited(Path::new(".").into(), "exit 42".to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
// Extract the shell name from path for cleaner test output
|
||||
let shell_path = std::env::var("SHELL").unwrap_or("bash".to_string());
|
||||
|
||||
let expected_output = format!(
|
||||
"Command failed with exit code 42 (shell: {}).\n\n```\n\n```",
|
||||
shell_path
|
||||
);
|
||||
assert_eq!(output, expected_output);
|
||||
}
|
||||
}
|
||||
9
crates/assistant_tools/src/terminal_tool/description.md
Normal file
9
crates/assistant_tools/src/terminal_tool/description.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Executes a shell one-liner and returns the combined output.
|
||||
|
||||
This tool spawns a process using the user's current shell, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
|
||||
|
||||
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
|
||||
|
||||
Do not use this tool for commands that run indefinitely, such as servers (e.g., `python -m http.server`) or file watchers that don't terminate on their own.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
|
||||
@@ -44,7 +44,7 @@ log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
prometheus = "0.14"
|
||||
prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
@@ -34,6 +34,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
|
||||
@@ -187,22 +187,20 @@ impl ComponentPreview {
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let known_scopes = [
|
||||
ComponentScope::Layout,
|
||||
ComponentScope::Input,
|
||||
ComponentScope::Editor,
|
||||
ComponentScope::Notification,
|
||||
ComponentScope::Collaboration,
|
||||
ComponentScope::VersionControl,
|
||||
ComponentScope::None,
|
||||
];
|
||||
|
||||
// Always show all components first
|
||||
entries.push(PreviewEntry::AllComponents);
|
||||
entries.push(PreviewEntry::Separator);
|
||||
|
||||
for scope in known_scopes.iter() {
|
||||
if let Some(components) = scope_groups.remove(scope) {
|
||||
let mut scopes: Vec<_> = scope_groups
|
||||
.keys()
|
||||
.filter(|scope| !matches!(**scope, ComponentScope::None))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
scopes.sort_by_key(|s| s.to_string());
|
||||
|
||||
for scope in scopes {
|
||||
if let Some(components) = scope_groups.remove(&scope) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
||||
let mut sorted_components = components;
|
||||
@@ -215,6 +213,7 @@ impl ComponentPreview {
|
||||
}
|
||||
}
|
||||
|
||||
// Add uncategorized components last
|
||||
if let Some(components) = scope_groups.get(&ComponentScope::None) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::Separator);
|
||||
@@ -272,7 +271,12 @@ impl ComponentPreview {
|
||||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::Separator => ListItem::new(ix)
|
||||
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
||||
.child(
|
||||
h_flex()
|
||||
.occlude()
|
||||
.pt_3()
|
||||
.child(Divider::horizontal_dashed()),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,12 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
cwd: Some(adapter_dir),
|
||||
arguments: Some(vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]})
|
||||
.to_string()
|
||||
.into(),
|
||||
]),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -117,6 +123,8 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
},
|
||||
});
|
||||
let map = args.as_object_mut().unwrap();
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
map.insert("name".into(), Value::String(config.label.clone()));
|
||||
match &config.request {
|
||||
DebugRequestType::Attach(attach) => {
|
||||
map.insert("pid".into(), attach.process_id.into());
|
||||
|
||||
@@ -417,7 +417,8 @@ impl DebugPanel {
|
||||
DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
label,
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
for session in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
@@ -425,11 +426,17 @@ impl DebugPanel {
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
move |_, cx| {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
let id: SharedString =
|
||||
format!("debug-session-{}", session.session_id(cx).0)
|
||||
.into();
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(cx))
|
||||
.child(
|
||||
@@ -437,15 +444,25 @@ impl DebugPanel {
|
||||
"close-debug-session",
|
||||
IconName::Close,
|
||||
)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, _, cx| {
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel
|
||||
.close_session(weak_session_id, cx);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod breakpoint_list;
|
||||
mod console;
|
||||
mod loaded_source_list;
|
||||
mod module_list;
|
||||
@@ -7,6 +8,7 @@ pub mod variable_list;
|
||||
use std::{any::Any, ops::ControlFlow, sync::Arc};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
use breakpoint_list::BreakpointList;
|
||||
use collections::HashMap;
|
||||
use console::Console;
|
||||
use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
|
||||
@@ -321,6 +323,21 @@ impl RunningState {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
breakpoints.focus_handle(cx),
|
||||
breakpoints.into(),
|
||||
SharedString::new_static("Breakpoints"),
|
||||
cx,
|
||||
)),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
});
|
||||
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
center_pane.update(cx, |this, cx| {
|
||||
|
||||
482
crates/debugger_ui/src/session/running/breakpoint_list.rs
Normal file
482
crates/debugger_ui/src/session/running/breakpoint_list.rs
Normal file
@@ -0,0 +1,482 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dap::ExceptionBreakpointsFilter;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity,
|
||||
list,
|
||||
};
|
||||
use language::Point;
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
|
||||
session::Session,
|
||||
},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use ui::{
|
||||
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
h_flex, px, v_flex,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(super) struct BreakpointList {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
list_state: ListState,
|
||||
scrollbar_state: ScrollbarState,
|
||||
breakpoints: Vec<BreakpointEntry>,
|
||||
session: Entity<Session>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
show_scrollbar: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointList {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl BreakpointList {
|
||||
pub(super) fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let project = project.read(cx);
|
||||
let breakpoint_store = project.breakpoint_store();
|
||||
let worktree_store = project.worktree_store();
|
||||
|
||||
cx.new(|cx| {
|
||||
let weak: gpui::WeakEntity<Self> = cx.weak_entity();
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, window, cx| {
|
||||
let Ok(Some(breakpoint)) =
|
||||
weak.update(cx, |this, _| this.breakpoints.get(ix).cloned())
|
||||
else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
breakpoint.render(window, cx).into_any_element()
|
||||
},
|
||||
);
|
||||
Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state: ScrollbarState::new(list_state.clone()),
|
||||
list_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SCROLLBAR_SHOW_INTERVAL)
|
||||
.await;
|
||||
panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.show_scrollbar = false;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("breakpoint-list-vertical-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Render for BreakpointList {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let old_len = self.breakpoints.len();
|
||||
let breakpoints = self.breakpoint_store.read(cx).all_breakpoints(cx);
|
||||
self.breakpoints.clear();
|
||||
let weak = cx.weak_entity();
|
||||
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
|
||||
let relative_worktree_path = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.find_worktree(&path, cx)
|
||||
.and_then(|(worktree, relative_path)| {
|
||||
worktree
|
||||
.read(cx)
|
||||
.is_visible()
|
||||
.then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
|
||||
});
|
||||
breakpoints.sort_by_key(|breakpoint| breakpoint.row);
|
||||
let weak = weak.clone();
|
||||
breakpoints.into_iter().filter_map(move |breakpoint| {
|
||||
debug_assert_eq!(&path, &breakpoint.path);
|
||||
let file_name = breakpoint.path.file_name()?;
|
||||
|
||||
let dir = relative_worktree_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
|
||||
.parent()
|
||||
.and_then(|parent| {
|
||||
parent
|
||||
.to_str()
|
||||
.map(ToOwned::to_owned)
|
||||
.map(SharedString::from)
|
||||
});
|
||||
let name = file_name
|
||||
.to_str()
|
||||
.map(ToOwned::to_owned)
|
||||
.map(SharedString::from)?;
|
||||
let weak = weak.clone();
|
||||
let line = format!("Line {}", breakpoint.row + 1).into();
|
||||
Some(BreakpointEntry {
|
||||
kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
|
||||
name,
|
||||
dir,
|
||||
line,
|
||||
breakpoint,
|
||||
}),
|
||||
weak,
|
||||
})
|
||||
})
|
||||
});
|
||||
let exception_breakpoints =
|
||||
self.session
|
||||
.read(cx)
|
||||
.exception_breakpoints()
|
||||
.map(|(data, is_enabled)| BreakpointEntry {
|
||||
kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
|
||||
id: data.filter.clone(),
|
||||
data: data.clone(),
|
||||
is_enabled: *is_enabled,
|
||||
}),
|
||||
weak: weak.clone(),
|
||||
});
|
||||
self.breakpoints
|
||||
.extend(breakpoints.chain(exception_breakpoints));
|
||||
if self.breakpoints.len() != old_len {
|
||||
self.list_state.reset(self.breakpoints.len());
|
||||
}
|
||||
v_flex()
|
||||
.id("breakpoint-list")
|
||||
.on_hover(cx.listener(|this, hovered, window, cx| {
|
||||
if *hovered {
|
||||
this.show_scrollbar = true;
|
||||
this.hide_scrollbar_task.take();
|
||||
cx.notify();
|
||||
} else if !this.focus_handle.contains_focused(window, cx) {
|
||||
this.hide_scrollbar(window, cx);
|
||||
}
|
||||
}))
|
||||
.size_full()
|
||||
.m_0p5()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
.children(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct LineBreakpoint {
|
||||
name: SharedString,
|
||||
dir: Option<SharedString>,
|
||||
line: SharedString,
|
||||
breakpoint: SourceBreakpoint,
|
||||
}
|
||||
|
||||
impl LineBreakpoint {
|
||||
fn render(self, weak: WeakEntity<BreakpointList>) -> ListItem {
|
||||
let LineBreakpoint {
|
||||
name,
|
||||
dir,
|
||||
line,
|
||||
breakpoint,
|
||||
} = self;
|
||||
let icon_name = if breakpoint.state.is_enabled() {
|
||||
IconName::DebugBreakpoint
|
||||
} else {
|
||||
IconName::DebugDisabledBreakpoint
|
||||
};
|
||||
let path = breakpoint.path;
|
||||
let row = breakpoint.row;
|
||||
let indicator = div()
|
||||
.id(SharedString::from(format!(
|
||||
"breakpoint-ui-toggle-{:?}/{}:{}",
|
||||
dir, name, line
|
||||
)))
|
||||
.cursor_pointer()
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
move |_, _, cx| {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.breakpoint_store.update(cx, |this, cx| {
|
||||
if let Some((buffer, breakpoint)) =
|
||||
this.breakpoint_at_row(&path, row, cx)
|
||||
{
|
||||
this.toggle_breakpoint(
|
||||
buffer,
|
||||
breakpoint,
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"breakpoint-ui-item-{:?}/{}:{}",
|
||||
dir, name, line
|
||||
)))
|
||||
.start_slot(indicator)
|
||||
.rounded()
|
||||
.end_hover_slot(
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
|
||||
dir, name, line
|
||||
)),
|
||||
IconName::Close,
|
||||
)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
move |_, _, cx| {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.breakpoint_store.update(cx, |this, cx| {
|
||||
if let Some((buffer, breakpoint)) =
|
||||
this.breakpoint_at_row(&path, row, cx)
|
||||
{
|
||||
this.toggle_breakpoint(
|
||||
buffer,
|
||||
breakpoint,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.icon_size(ui::IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
|
||||
dir, name, line
|
||||
)))
|
||||
.on_click(move |_, window, cx| {
|
||||
let path = path.clone();
|
||||
let weak = weak.clone();
|
||||
let row = breakpoint.row;
|
||||
maybe!({
|
||||
let task = weak
|
||||
.update(cx, |this, cx| {
|
||||
this.worktree_store.update(cx, |this, cx| {
|
||||
this.find_or_create_worktree(path, false, cx)
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let (worktree, relative_path) = task.await?;
|
||||
let worktree_id = worktree.update(cx, |this, _| this.id())?;
|
||||
let item = weak
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |this, cx| {
|
||||
this.open_path(
|
||||
(worktree_id, relative_path),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
if let Some(editor) = item.downcast::<Editor>() {
|
||||
editor
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.go_to_singleton_buffer_point(
|
||||
Point { row, column: 0 },
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Some(())
|
||||
});
|
||||
})
|
||||
.cursor_pointer()
|
||||
.py_1()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(name)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
)
|
||||
.children(dir.map(|dir| {
|
||||
Label::new(dir)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Label::new(line)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExceptionBreakpoint {
|
||||
id: String,
|
||||
data: ExceptionBreakpointsFilter,
|
||||
is_enabled: bool,
|
||||
}
|
||||
|
||||
impl ExceptionBreakpoint {
|
||||
fn render(self, list: WeakEntity<BreakpointList>) -> ListItem {
|
||||
let color = if self.is_enabled {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
let id = SharedString::from(&self.id);
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}",
|
||||
self.id
|
||||
)))
|
||||
.rounded()
|
||||
.start_slot(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}-click-handler",
|
||||
self.id
|
||||
)))
|
||||
.on_click(move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
this.session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(self.data.label)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
)
|
||||
.children(self.data.description.map(|description| {
|
||||
Label::new(description)
|
||||
.size(LabelSize::XSmall)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
.color(Color::Muted)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
enum BreakpointEntryKind {
|
||||
LineBreakpoint(LineBreakpoint),
|
||||
ExceptionBreakpoint(ExceptionBreakpoint),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BreakpointEntry {
|
||||
kind: BreakpointEntryKind,
|
||||
weak: WeakEntity<BreakpointList>,
|
||||
}
|
||||
impl RenderOnce for BreakpointEntry {
|
||||
fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement {
|
||||
match self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.render(self.weak)
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
exception_breakpoint.render(self.weak)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ use project::{
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc, usize};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{Divider, prelude::*};
|
||||
|
||||
pub struct Console {
|
||||
console: Entity<Editor>,
|
||||
@@ -229,7 +229,8 @@ impl Render for Console {
|
||||
.size_full()
|
||||
.child(self.render_console(cx))
|
||||
.when(self.is_local(cx), |this| {
|
||||
this.child(self.render_query_bar(cx))
|
||||
this.child(Divider::horizontal())
|
||||
.child(self.render_query_bar(cx))
|
||||
.pt(DynamicSpacing::Base04.rems(cx))
|
||||
})
|
||||
.border_2()
|
||||
|
||||
@@ -3130,6 +3130,7 @@ impl Editor {
|
||||
let mut new_selections = Vec::with_capacity(selections.len());
|
||||
let mut new_autoclose_regions = Vec::new();
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
let mut clear_linked_edit_ranges = false;
|
||||
|
||||
for (selection, autoclose_region) in
|
||||
self.selections_with_autoclose_regions(selections, &snapshot)
|
||||
@@ -3357,6 +3358,8 @@ impl Editor {
|
||||
.extend(edits.into_iter().map(|range| (range, text.clone())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clear_linked_edit_ranges = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3367,6 +3370,9 @@ impl Editor {
|
||||
drop(snapshot);
|
||||
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
if clear_linked_edit_ranges {
|
||||
this.linked_edit_ranges.clear();
|
||||
}
|
||||
let initial_buffer_versions =
|
||||
jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
|
||||
|
||||
@@ -5166,13 +5172,16 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let newest_selection = self.selections.newest_anchor().clone();
|
||||
let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone();
|
||||
let buffer = self.buffer.read(cx);
|
||||
if newest_selection.head().diff_base_anchor.is_some() {
|
||||
return None;
|
||||
}
|
||||
let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
|
||||
let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?;
|
||||
let (start_buffer, start) =
|
||||
buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
|
||||
let (end_buffer, end) =
|
||||
buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
@@ -6365,20 +6374,9 @@ impl Editor {
|
||||
breakpoint_display_points
|
||||
}
|
||||
|
||||
fn edit_breakpoint_context_menu(
|
||||
fn breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn basic_breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
edit_menu_position: (Anchor, gpui::Point<Pixels>),
|
||||
is_edit_menu: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
@@ -6396,6 +6394,12 @@ impl Editor {
|
||||
.breakpoint_at_row(row, window, cx)
|
||||
.map(|(anchor, bp)| (anchor, Arc::from(bp)));
|
||||
|
||||
let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) {
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Set Log Breakpoint"
|
||||
};
|
||||
|
||||
let condition_breakpoint_msg = if breakpoint
|
||||
.as_ref()
|
||||
.is_some_and(|bp| bp.1.condition.is_some())
|
||||
@@ -6423,13 +6427,9 @@ impl Editor {
|
||||
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
|
||||
|
||||
let breakpoint_is_at_row = breakpoint.is_some();
|
||||
let log_editor = cx.new(|cx| {
|
||||
let mut log_editor = Editor::single_line(window, cx);
|
||||
if let Some(text) = &breakpoint.as_ref().and_then(|bp| bp.1.message.clone()) {
|
||||
log_editor.insert(text.as_ref(), window, cx);
|
||||
}
|
||||
log_editor
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
BreakpointState::Disabled => Some("Enable"),
|
||||
});
|
||||
|
||||
let (anchor, breakpoint) =
|
||||
@@ -6453,73 +6453,8 @@ impl Editor {
|
||||
})
|
||||
.separator()
|
||||
})
|
||||
.entry("edit breakpoint", None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let context_menu =
|
||||
editor.edit_breakpoint_context_menu(anchor, window, cx);
|
||||
let (source, clicked_point) = edit_menu_position;
|
||||
|
||||
editor.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
editor,
|
||||
source,
|
||||
clicked_point,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.when(breakpoint_is_at_row, |this| {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
this.custom_row(move |window, cx| {
|
||||
let breakpoint = weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.breakpoint_at_row(row, window, cx).map(|bp| bp.1)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| breakpoint.as_ref().clone());
|
||||
|
||||
let is_enabled = match breakpoint.is_enabled() {
|
||||
true => ToggleState::Selected,
|
||||
false => ToggleState::Unselected,
|
||||
};
|
||||
|
||||
ui::CheckboxWithLabel::new(
|
||||
"enable-breakpoint",
|
||||
Label::new("Enable"),
|
||||
is_enabled,
|
||||
{
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |_, _, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.clone(),
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(IconPosition::End)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.when(!breakpoint_is_at_row, |this| {
|
||||
this.entry(set_breakpoint_msg, None, {
|
||||
.when_some(toggle_state_msg, |this, msg| {
|
||||
this.entry(msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
@@ -6528,7 +6463,7 @@ impl Editor {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
@@ -6536,72 +6471,69 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(is_edit_menu, |this| {
|
||||
this.custom_entry(
|
||||
{
|
||||
breakpoint_message_editor_element(
|
||||
anchor,
|
||||
breakpoint.clone(),
|
||||
BreakpointMessageKind::Log,
|
||||
log_editor.clone(),
|
||||
weak_editor.clone(),
|
||||
)
|
||||
},
|
||||
{
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
let log_editor = log_editor.clone();
|
||||
|
||||
move |_, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(
|
||||
log_message.into(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.entry(set_breakpoint_msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(log_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Log,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(condition_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Condition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::HitCondition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
|
||||
// .entry(condition_breakpoint_msg, None, {
|
||||
// let breakpoint = breakpoint.clone();
|
||||
// let weak_editor = weak_editor.clone();
|
||||
// move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::Condition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// }
|
||||
// })
|
||||
// .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::HitCondition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8822,48 +8754,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint_menu_editor(
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: AbsoluteLength::Rems(Rems(0.75)),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
background_color: Some(theme.colors().editor_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let element = EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
div()
|
||||
.rounded_sm()
|
||||
.when(
|
||||
editor.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| this.border_color(theme.colors().border_focused),
|
||||
)
|
||||
.child(element)
|
||||
.min_w(Pixels(300.0))
|
||||
.size_full()
|
||||
.bg(theme.colors().editor_background)
|
||||
.h_4()
|
||||
}
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
display_row: DisplayRow,
|
||||
@@ -8881,13 +8771,7 @@ impl Editor {
|
||||
.snapshot(cx)
|
||||
.anchor_before(Point::new(display_row.0, 0u32));
|
||||
|
||||
let context_menu = self.basic_breakpoint_context_menu(
|
||||
position.unwrap_or(source),
|
||||
(source, clicked_point),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
@@ -14300,12 +14184,28 @@ impl Editor {
|
||||
}
|
||||
};
|
||||
|
||||
let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx));
|
||||
let selections_prev = transaction_id_prev
|
||||
.and_then(|transaction_id_prev| {
|
||||
// default to selections as they were after the last edit, if we have them,
|
||||
// instead of how they are now.
|
||||
// This will make it so that editing, moving somewhere else, formatting, then undoing the format
|
||||
// will take you back to where you made the last edit, instead of staying where you scrolled
|
||||
self.selection_history
|
||||
.transaction(transaction_id_prev)
|
||||
.map(|t| t.0.clone())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated");
|
||||
self.selections.disjoint_anchors()
|
||||
});
|
||||
|
||||
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, target, true, trigger, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let transaction = futures::select_biased! {
|
||||
transaction = format.log_err().fuse() => transaction,
|
||||
() = timeout => {
|
||||
@@ -14325,6 +14225,19 @@ impl Editor {
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Some(transaction_id_now) =
|
||||
buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))?
|
||||
{
|
||||
let has_new_transaction = transaction_id_prev != Some(transaction_id_now);
|
||||
if has_new_transaction {
|
||||
_ = editor.update(cx, |editor, _| {
|
||||
editor
|
||||
.selection_history
|
||||
.insert_transaction(transaction_id_now, selections_prev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -18134,58 +18047,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
enum BreakpointMessageKind {
|
||||
Log,
|
||||
Conditional,
|
||||
HitConditional,
|
||||
}
|
||||
|
||||
fn breakpoint_message_editor_element(
|
||||
anchor: Anchor,
|
||||
breakpoint: Arc<Breakpoint>,
|
||||
message_kind: BreakpointMessageKind,
|
||||
message_editor: Entity<Editor>,
|
||||
weak_editor: WeakEntity<Editor>,
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyElement {
|
||||
let log_editor = message_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
move |window, cx| {
|
||||
let label = Label::new("Log message");
|
||||
let log_editor = log_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.size_full()
|
||||
.child(label)
|
||||
.child(Editor::render_breakpoint_menu_editor(
|
||||
&log_editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_action(move |_: &menu::Confirm, _, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
this.mouse_context_menu = None;
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
// Consider user intent and default settings
|
||||
fn choose_completion_range(
|
||||
completion: &Completion,
|
||||
|
||||
@@ -5873,6 +5873,83 @@ async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
line 1
|
||||
line 2
|
||||
linˇe 3
|
||||
line 4
|
||||
line 5
|
||||
"});
|
||||
|
||||
// Make an edit
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("X", window, cx);
|
||||
});
|
||||
|
||||
// Move cursor to a different position
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
|
||||
});
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 1
|
||||
line 2
|
||||
linXe 3
|
||||
line 4
|
||||
liˇne 5
|
||||
"});
|
||||
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
"PREFIX ".to_string(),
|
||||
)]))
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
PREFIX line 1
|
||||
line 2
|
||||
linXe 3
|
||||
line 4
|
||||
liˇne 5
|
||||
"});
|
||||
|
||||
// Undo formatting
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.undo(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
// Verify cursor moved back to position after edit
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 1
|
||||
line 2
|
||||
linXˇe 3
|
||||
line 4
|
||||
line 5
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -7756,77 +7833,81 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
fake_server
|
||||
.set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
assert_eq!(params.options.tab_size, 4);
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
|
||||
", ".to_string(),
|
||||
)]))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
{
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
assert_eq!(params.options.tab_size, 4);
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
|
||||
", ".to_string(),
|
||||
)]))
|
||||
},
|
||||
);
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"one, two\nthree\n"
|
||||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"one, two\nthree\n"
|
||||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
}
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("one\ntwo\nthree\n", window, cx)
|
||||
});
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
{
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("one\ntwo\nthree\n", window, cx)
|
||||
});
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Ensure we can still save even if formatting hangs.
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
},
|
||||
);
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
// Ensure we can still save even if formatting hangs.
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
},
|
||||
);
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
}
|
||||
|
||||
// For non-dirty buffer, no formatting request should be sent
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
let _pending_format_request = fake_server
|
||||
.set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
|
||||
{
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
|
||||
panic!("Should not be invoked on non-dirty buffer");
|
||||
})
|
||||
.next();
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
});
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
}
|
||||
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_language_settings(cx, |settings| {
|
||||
@@ -7839,28 +7920,28 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
);
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("somehting_new\n", window, cx)
|
||||
});
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
fake_server
|
||||
.set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
assert_eq!(params.options.tab_size, 8);
|
||||
Ok(Some(vec![]))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
{
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("somehting_new\n", window, cx)
|
||||
});
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
let _formatting_request_signal = fake_server
|
||||
.set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
|
||||
);
|
||||
assert_eq!(params.options.tab_size, 8);
|
||||
Ok(Some(vec![]))
|
||||
});
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(true, project.clone(), window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().start_waiting();
|
||||
save.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -8342,6 +8423,272 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
settings.defaults.formatter =
|
||||
Some(language_settings::SelectedFormatter::List(FormatterList(
|
||||
vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::CodeActions(
|
||||
[
|
||||
("code-action-1".into(), true),
|
||||
("code-action-2".into(), true),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
)))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec!["the-command-for-code-action-1".into()],
|
||||
..Default::default()
|
||||
}),
|
||||
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/file.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
build_editor_with_project(project.clone(), buffer, window, cx)
|
||||
});
|
||||
|
||||
cx.executor().start_waiting();
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |_params, _| async move {
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
"applied-formatting\n".to_string(),
|
||||
)]))
|
||||
},
|
||||
);
|
||||
fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
|
||||
move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.context.only,
|
||||
Some(vec!["code-action-1".into(), "code-action-2".into()])
|
||||
);
|
||||
let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
|
||||
Ok(Some(vec![
|
||||
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
|
||||
kind: Some("code-action-1".into()),
|
||||
edit: Some(lsp::WorkspaceEdit::new(
|
||||
[(
|
||||
uri.clone(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
"applied-code-action-1-edit\n".to_string(),
|
||||
)],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)),
|
||||
command: Some(lsp::Command {
|
||||
command: "the-command-for-code-action-1".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
|
||||
kind: Some("code-action-2".into()),
|
||||
edit: Some(lsp::WorkspaceEdit::new(
|
||||
[(
|
||||
uri.clone(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
"applied-code-action-2-edit\n".to_string(),
|
||||
)],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
]))
|
||||
},
|
||||
);
|
||||
|
||||
fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
|
||||
move |params, _| async move { Ok(params) }
|
||||
});
|
||||
|
||||
let command_lock = Arc::new(futures::lock::Mutex::new(()));
|
||||
fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
|
||||
let fake = fake_server.clone();
|
||||
let lock = command_lock.clone();
|
||||
move |params, _| {
|
||||
assert_eq!(params.command, "the-command-for-code-action-1");
|
||||
let fake = fake.clone();
|
||||
let lock = lock.clone();
|
||||
async move {
|
||||
lock.lock().await;
|
||||
fake.server
|
||||
.request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
|
||||
label: None,
|
||||
edit: lsp::WorkspaceEdit {
|
||||
changes: Some(
|
||||
[(
|
||||
lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
|
||||
vec![lsp::TextEdit {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 0),
|
||||
lsp::Position::new(0, 0),
|
||||
),
|
||||
new_text: "applied-code-action-1-command\n".into(),
|
||||
}],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.executor().start_waiting();
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
project.clone(),
|
||||
FormatTrigger::Manual,
|
||||
FormatTarget::Buffers,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
r#"
|
||||
applied-code-action-2-edit
|
||||
applied-code-action-1-command
|
||||
applied-code-action-1-edit
|
||||
applied-formatting
|
||||
one
|
||||
two
|
||||
three
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.undo(&Default::default(), window, cx);
|
||||
assert_eq!(editor.text(cx), "one \ntwo \nthree");
|
||||
});
|
||||
|
||||
// Perform a manual edit while waiting for an LSP command
|
||||
// that's being run as part of a formatting code action.
|
||||
let lock_guard = command_lock.lock().await;
|
||||
let format = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
project.clone(),
|
||||
FormatTrigger::Manual,
|
||||
FormatTarget::Buffers,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
r#"
|
||||
applied-code-action-1-edit
|
||||
applied-formatting
|
||||
one
|
||||
two
|
||||
three
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
let ix = buffer.len(cx);
|
||||
buffer.edit([(ix..ix, "edited\n")], None, cx);
|
||||
});
|
||||
});
|
||||
|
||||
// Allow the LSP command to proceed. Because the buffer was edited,
|
||||
// the second code action will not be run.
|
||||
drop(lock_guard);
|
||||
format.await;
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
r#"
|
||||
applied-code-action-1-command
|
||||
applied-code-action-1-edit
|
||||
applied-formatting
|
||||
one
|
||||
two
|
||||
three
|
||||
edited
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
|
||||
// The manual edit is undone first, because it is the last thing the user did
|
||||
// (even though the command completed afterwards).
|
||||
editor.undo(&Default::default(), window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
r#"
|
||||
applied-code-action-1-command
|
||||
applied-code-action-1-edit
|
||||
applied-formatting
|
||||
one
|
||||
two
|
||||
three
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
|
||||
// All the formatting (including the command, which completed after the manual edit)
|
||||
// is undone together.
|
||||
editor.undo(&Default::default(), window, cx);
|
||||
assert_eq!(editor.text(cx), "one \ntwo \nthree");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
|
||||
@@ -3890,15 +3890,7 @@ impl EditorElement {
|
||||
)
|
||||
})?;
|
||||
|
||||
element.prepaint_as_root(
|
||||
position,
|
||||
Size {
|
||||
width: AvailableSpace::Definite(Pixels(100.)),
|
||||
height: AvailableSpace::MinContent,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
|
||||
Some(element)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1540,8 +1540,24 @@ impl SearchableItem for Editor {
|
||||
let text = self.buffer.read(cx);
|
||||
let text = text.snapshot(cx);
|
||||
let mut edits = vec![];
|
||||
let mut last_point: Option<Point> = None;
|
||||
|
||||
for m in matches {
|
||||
let point = m.start.to_point(&text);
|
||||
let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
|
||||
|
||||
// Check if the row for the current match is different from the last
|
||||
// match. If that's not the case and we're still replacing matches
|
||||
// in the same row/line, skip this match if the `one_match_per_line`
|
||||
// option is enabled.
|
||||
if last_point.is_none() {
|
||||
last_point = Some(point);
|
||||
} else if last_point.is_some() && point.row != last_point.unwrap().row {
|
||||
last_point = Some(point);
|
||||
} else if query.one_match_per_line().is_some_and(|enabled| enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let text: Cow<_> = if text.len() == 1 {
|
||||
text.first().cloned().unwrap().into()
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,10 @@ impl LinkedEditingRanges {
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -85,6 +85,10 @@ pub fn lsp_tasks(
|
||||
.map(|(name, buffer_ids)| {
|
||||
let buffers = buffer_ids
|
||||
.iter()
|
||||
.filter(|&&buffer_id| match for_position {
|
||||
Some(for_position) => for_position.buffer_id == Some(buffer_id),
|
||||
None => true,
|
||||
})
|
||||
.filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx))
|
||||
.collect::<Vec<_>>();
|
||||
language_server_for_buffers(project.clone(), name.clone(), buffers, cx)
|
||||
|
||||
39
crates/eval/Cargo.toml
Normal file
39
crates/eval/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "eval"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
env_logger.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
node_runtime.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
toml.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "eval"
|
||||
path = "src/eval.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/eval/LICENSE-GPL
Symbolic link
1
crates/eval/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
7
crates/eval/README.md
Normal file
7
crates/eval/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Eval
|
||||
|
||||
This eval assumes the working directory is the root of the repository. Run it with:
|
||||
|
||||
```sh
|
||||
cargo run -p eval
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
path = "../zed_worktree"
|
||||
revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
|
||||
@@ -0,0 +1,3 @@
|
||||
Look at the `find_replace_file_tool.rs`. I want to implement a card for it. The card should be a brand new `Entity` with a `Render` implementation.
|
||||
|
||||
The card should show a diff. It should be a beautifully presented diff. The card "box" should look like what we show for markdown codeblocks (look at `MarkdownElement`). I want to see a red background for lines that were deleted and a green background for lines that were added. We should have a div per diff line.
|
||||
229
crates/eval/src/agent.rs
Normal file
229
crates/eval/src/agent.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use ::agent::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use dap::DapRegistry;
|
||||
use gpui::{App, Entity, SemanticVersion, Subscription, Task, prelude::*};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{Project, RealFs};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::SettingsStore;
|
||||
use smol::channel;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.
|
||||
pub struct AgentAppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: Entity<UserStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub node_runtime: NodeRuntime,
|
||||
|
||||
// Additional fields not present in `workspace::AppState`.
|
||||
pub prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
pub struct Agent {
|
||||
// pub thread: Entity<Thread>,
|
||||
// pub project: Entity<Project>,
|
||||
#[allow(dead_code)]
|
||||
pub thread_store: Entity<ThreadStore>,
|
||||
pub tool_use_counts: HashMap<Arc<str>, u32>,
|
||||
pub done_tx: channel::Sender<anyhow::Result<()>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub fn new(
|
||||
app_state: Arc<AgentAppState>,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<(Entity<Self>, channel::Receiver<anyhow::Result<()>>)> {
|
||||
let env = None;
|
||||
let project = Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
Arc::new(DapRegistry::default()),
|
||||
app_state.fs.clone(),
|
||||
env,
|
||||
cx,
|
||||
);
|
||||
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store =
|
||||
ThreadStore::new(project.clone(), tools, app_state.prompt_builder.clone(), cx)?;
|
||||
|
||||
let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
|
||||
|
||||
let (done_tx, done_rx) = channel::unbounded::<anyhow::Result<()>>();
|
||||
|
||||
let headless_thread = cx.new(move |cx| Self {
|
||||
_subscription: cx.subscribe(&thread, Self::handle_thread_event),
|
||||
// thread,
|
||||
// project,
|
||||
thread_store,
|
||||
tool_use_counts: HashMap::default(),
|
||||
done_tx,
|
||||
});
|
||||
|
||||
Ok((headless_thread, done_rx))
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
thread: Entity<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(err) => self
|
||||
.done_tx
|
||||
.send_blocking(Err(anyhow!("{:?}", err)))
|
||||
.unwrap(),
|
||||
ThreadEvent::DoneStreaming => {
|
||||
let thread = thread.read(cx);
|
||||
if let Some(message) = thread.messages().last() {
|
||||
println!("Message: {}", message.to_string());
|
||||
}
|
||||
if thread.all_tools_finished() {
|
||||
self.done_tx.send_blocking(Ok(())).unwrap()
|
||||
}
|
||||
}
|
||||
ThreadEvent::UsePendingTools { .. } => {}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
// Automatically approve all tools that need confirmation in headless mode
|
||||
println!("Tool confirmation needed - automatically approving in headless mode");
|
||||
|
||||
// Get the tools needing confirmation
|
||||
let tools_needing_confirmation: Vec<_> = thread
|
||||
.read(cx)
|
||||
.tools_needing_confirmation()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Run each tool that needs confirmation
|
||||
for tool_use in tools_needing_confirmation {
|
||||
if let Some(tool) = thread.read(cx).tools().tool(&tool_use.name, cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
println!("Auto-approving tool: {}", tool_use.name);
|
||||
|
||||
// Create a request to send to the tool
|
||||
let request = thread.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
|
||||
// Run the tool
|
||||
thread.run_tool(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
tool,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
..
|
||||
} => {
|
||||
if let Some(pending_tool_use) = pending_tool_use {
|
||||
println!(
|
||||
"Used tool {} with input: {}",
|
||||
pending_tool_use.name, pending_tool_use.input
|
||||
);
|
||||
*self
|
||||
.tool_use_counts
|
||||
.entry(pending_tool_use.name.clone())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
|
||||
println!("Tool result: {:?}", tool_result);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
gpui_tokio::init(cx);
|
||||
|
||||
let mut settings_store = SettingsStore::new(cx);
|
||||
settings_store
|
||||
.set_default_settings(settings::default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(settings_store);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
cx.set_http_client(client.http_client().clone());
|
||||
|
||||
let git_binary_path = None;
|
||||
let fs = Arc::new(RealFs::new(
|
||||
git_binary_path,
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
assistant_tools::init(client.http_client().clone(), cx);
|
||||
context_server::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
|
||||
|
||||
Arc::new(AgentAppState {
|
||||
languages,
|
||||
client,
|
||||
user_store,
|
||||
fs,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
prompt_builder,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_model(model_name: &str, cx: &App) -> anyhow::Result<Arc<dyn LanguageModel>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry
|
||||
.available_models(cx)
|
||||
.find(|model| model.id().0 == model_name);
|
||||
|
||||
let Some(model) = model else {
|
||||
return Err(anyhow!(
|
||||
"No language model named {} was available. Available models: {}",
|
||||
model_name,
|
||||
model_registry
|
||||
.available_models(cx)
|
||||
.map(|model| model.id().0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
};
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub fn authenticate_model_provider(
|
||||
provider_id: LanguageModelProviderId,
|
||||
cx: &mut App,
|
||||
) -> Task<std::result::Result<(), AuthenticateError>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model_provider = model_registry.provider(&provider_id).unwrap();
|
||||
model_provider.authenticate(cx)
|
||||
}
|
||||
101
crates/eval/src/eval.rs
Normal file
101
crates/eval/src/eval.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use agent::Agent;
|
||||
use anyhow::Result;
|
||||
use gpui::Application;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
mod agent;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ExampleBase {
|
||||
pub path: PathBuf,
|
||||
pub revision: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Example {
|
||||
pub base: ExampleBase,
|
||||
|
||||
/// Content of the prompt.md file
|
||||
pub prompt: String,
|
||||
|
||||
/// Content of the rubric.md file
|
||||
pub rubric: String,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
/// Load an example from a directory containing base.toml, prompt.md, and rubric.md
|
||||
pub fn load_from_directory<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
|
||||
let base_path = dir_path.as_ref().join("base.toml");
|
||||
let prompt_path = dir_path.as_ref().join("prompt.md");
|
||||
let rubric_path = dir_path.as_ref().join("rubric.md");
|
||||
|
||||
let mut base: ExampleBase = toml::from_str(&fs::read_to_string(&base_path)?)?;
|
||||
base.path = base.path.canonicalize()?;
|
||||
|
||||
Ok(Example {
|
||||
base,
|
||||
prompt: fs::read_to_string(prompt_path)?,
|
||||
rubric: fs::read_to_string(rubric_path)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set up the example by checking out the specified Git revision
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
use std::process::Command;
|
||||
|
||||
// Check if the directory exists
|
||||
let path = Path::new(&self.base.path);
|
||||
anyhow::ensure!(path.exists(), "Path does not exist: {:?}", self.base.path);
|
||||
|
||||
// Change to the project directory and checkout the specified revision
|
||||
let output = Command::new("git")
|
||||
.current_dir(&self.base.path)
|
||||
.arg("checkout")
|
||||
.arg(&self.base.revision)
|
||||
.output()?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Failed to checkout revision {}: {}",
|
||||
self.base.revision,
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let http_client = Arc::new(ReqwestClient::new());
|
||||
let app = Application::headless().with_http_client(http_client.clone());
|
||||
|
||||
app.run(move |cx| {
|
||||
let app_state = crate::agent::init(cx);
|
||||
let _agent = Agent::new(app_state, cx);
|
||||
|
||||
let model = agent::find_model("claude-3-7-sonnet-thinking-latest", cx).unwrap();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(Some(model.clone()), cx);
|
||||
});
|
||||
|
||||
let model_provider_id = model.provider_id();
|
||||
|
||||
let authenticate = agent::authenticate_model_provider(model_provider_id.clone(), cx);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
authenticate.await.unwrap();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// let example =
|
||||
// Example::load_from_directory("./crates/eval/examples/find_and_replace_diff_card")?;
|
||||
// example.setup()?;
|
||||
}
|
||||
@@ -95,6 +95,15 @@ impl FeatureFlag for Debugger {
|
||||
const NAME: &'static str = "debugger";
|
||||
}
|
||||
|
||||
pub struct ThreadAutoCapture {}
|
||||
impl FeatureFlag for ThreadAutoCapture {
|
||||
const NAME: &'static str = "thread-auto-capture";
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -8,11 +8,13 @@ struct HelloWorld {}
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.size_full()
|
||||
.child(div().child("Text left"))
|
||||
.child(div().text_center().child("Text center"))
|
||||
@@ -71,6 +73,24 @@ impl Render for HelloWorld {
|
||||
.child("100%"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("Text Link")
|
||||
.text_color(gpui::blue())
|
||||
.cursor_pointer()
|
||||
.active(|this| {
|
||||
this.text_color(gpui::white())
|
||||
.bg(gpui::blue())
|
||||
.text_decoration_1()
|
||||
.text_decoration_wavy()
|
||||
})
|
||||
.hover(|this| {
|
||||
this.text_color(gpui::rgb(0x973717))
|
||||
.bg(gpui::yellow())
|
||||
.text_decoration_1()
|
||||
})
|
||||
.child("Text with hover, active styles"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1662,7 +1662,7 @@ impl Interactivity {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
use crate::{BorderStyle, TextAlign};
|
||||
use crate::BorderStyle;
|
||||
|
||||
if global_id.is_some()
|
||||
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
|
||||
@@ -1685,7 +1685,7 @@ impl Interactivity {
|
||||
.ok()
|
||||
.and_then(|mut text| text.pop())
|
||||
{
|
||||
text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx)
|
||||
text.paint(hitbox.origin, FONT_SIZE, None, None, window, cx)
|
||||
.ok();
|
||||
|
||||
let text_bounds = crate::Bounds {
|
||||
|
||||
@@ -415,7 +415,9 @@ impl TextLayout {
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
|
||||
let text_style = window.text_style();
|
||||
|
||||
for line in &element_state.lines {
|
||||
line.paint_background(
|
||||
line_origin,
|
||||
@@ -429,7 +431,7 @@ impl TextLayout {
|
||||
line.paint(
|
||||
line_origin,
|
||||
line_height,
|
||||
text_style.text_align,
|
||||
Some(&text_style),
|
||||
Some(bounds),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{MacDisplay, NSRange, NSStringExt, ns_string, renderer};
|
||||
use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
|
||||
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
@@ -1021,11 +1021,8 @@ impl PlatformWindow for MacWindow {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let opaque = if background_appearance == WindowBackgroundAppearance::Opaque {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
|
||||
|
||||
unsafe {
|
||||
this.native_window.setOpaque_(opaque);
|
||||
// Shadows for transparent windows cause artifacts and performance issues
|
||||
@@ -1981,14 +1978,11 @@ extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
if send_new_event(
|
||||
send_new_event(
|
||||
&window_state,
|
||||
PlatformInput::FileDrop(FileDropEvent::Submit { position }),
|
||||
) {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
)
|
||||
.to_objc()
|
||||
}
|
||||
|
||||
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
App, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result, SharedString, StrikethroughStyle,
|
||||
TextAlign, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout, black, fill, point, px,
|
||||
size,
|
||||
TextAlign, TextStyle, UnderlineStyle, Window, WrapBoundary, WrappedLineLayout, black, fill,
|
||||
point, px, size,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
@@ -71,7 +71,7 @@ impl ShapedLine {
|
||||
origin,
|
||||
&self.layout,
|
||||
line_height,
|
||||
TextAlign::default(),
|
||||
None,
|
||||
None,
|
||||
&self.decoration_runs,
|
||||
&[],
|
||||
@@ -125,11 +125,12 @@ impl WrappedLine {
|
||||
}
|
||||
|
||||
/// Paint this line of text to the window.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
align: TextAlign,
|
||||
text_style: Option<&TextStyle>,
|
||||
bounds: Option<Bounds<Pixels>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -143,7 +144,7 @@ impl WrappedLine {
|
||||
origin,
|
||||
&self.layout.unwrapped_layout,
|
||||
line_height,
|
||||
align,
|
||||
text_style,
|
||||
align_width,
|
||||
&self.decoration_runs,
|
||||
&self.wrap_boundaries,
|
||||
@@ -189,7 +190,7 @@ fn paint_line(
|
||||
origin: Point<Pixels>,
|
||||
layout: &LineLayout,
|
||||
line_height: Pixels,
|
||||
align: TextAlign,
|
||||
text_style: Option<&TextStyle>,
|
||||
align_width: Option<Pixels>,
|
||||
decoration_runs: &[DecorationRun],
|
||||
wrap_boundaries: &[WrapBoundary],
|
||||
@@ -203,6 +204,10 @@ fn paint_line(
|
||||
line_height * (wrap_boundaries.len() as f32 + 1.),
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: text_align and line_height need to inherit from normal style when is hovered or activated.
|
||||
let mut text_align = text_style.map(|s| s.text_align).unwrap_or(TextAlign::Left);
|
||||
|
||||
window.paint_layer(line_bounds, |window| {
|
||||
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
|
||||
let baseline_offset = point(px(0.), padding_top + layout.ascent);
|
||||
@@ -218,7 +223,7 @@ fn paint_line(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
px(0.0),
|
||||
&align,
|
||||
&text_align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
),
|
||||
@@ -269,7 +274,7 @@ fn paint_line(
|
||||
origin,
|
||||
align_width.unwrap_or(layout.width),
|
||||
glyph.position.x,
|
||||
&align,
|
||||
&text_align,
|
||||
layout,
|
||||
wraps.peek(),
|
||||
);
|
||||
@@ -292,30 +297,44 @@ fn paint_line(
|
||||
}
|
||||
|
||||
if let Some(style_run) = style_run {
|
||||
let mut run_color = style_run.color;
|
||||
let mut run_underline = style_run.underline.as_ref();
|
||||
let mut run_strikethrough = style_run.strikethrough;
|
||||
// Override by text run by current style when hovered or activated.
|
||||
if let Some(val) = text_style.map(|s| s.color) {
|
||||
run_color = val;
|
||||
}
|
||||
if let Some(val) = text_style.and_then(|s| s.underline.as_ref()) {
|
||||
run_underline = Some(val);
|
||||
}
|
||||
if let Some(val) = text_style.and_then(|s| s.strikethrough) {
|
||||
run_strikethrough = Some(val);
|
||||
}
|
||||
|
||||
if let Some((_, underline_style)) = &mut current_underline {
|
||||
if style_run.underline.as_ref() != Some(underline_style) {
|
||||
finished_underline = current_underline.take();
|
||||
}
|
||||
}
|
||||
if let Some(run_underline) = style_run.underline.as_ref() {
|
||||
if let Some(run_underline) = run_underline.as_ref() {
|
||||
current_underline.get_or_insert((
|
||||
point(
|
||||
glyph_origin.x,
|
||||
glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
|
||||
),
|
||||
UnderlineStyle {
|
||||
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
||||
color: Some(run_underline.color.unwrap_or(run_color)),
|
||||
thickness: run_underline.thickness,
|
||||
wavy: run_underline.wavy,
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Some((_, strikethrough_style)) = &mut current_strikethrough {
|
||||
if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
|
||||
if run_strikethrough.as_ref() != Some(strikethrough_style) {
|
||||
finished_strikethrough = current_strikethrough.take();
|
||||
}
|
||||
}
|
||||
if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
|
||||
if let Some(mut run_strikethrough) = run_strikethrough.as_ref() {
|
||||
current_strikethrough.get_or_insert((
|
||||
point(
|
||||
glyph_origin.x,
|
||||
@@ -323,14 +342,14 @@ fn paint_line(
|
||||
+ (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
|
||||
),
|
||||
StrikethroughStyle {
|
||||
color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
|
||||
color: Some(run_strikethrough.color.unwrap_or(run_color)),
|
||||
thickness: run_strikethrough.thickness,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
run_end += style_run.len as usize;
|
||||
color = style_run.color;
|
||||
color = run_color;
|
||||
} else {
|
||||
run_end = layout.len;
|
||||
finished_underline = current_underline.take();
|
||||
|
||||
@@ -845,6 +845,7 @@ impl Window {
|
||||
handle
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
window.active.set(active);
|
||||
window.modifiers = window.platform_window.modifiers();
|
||||
window
|
||||
.activation_observers
|
||||
.clone()
|
||||
|
||||
@@ -39,6 +39,7 @@ pub enum IconName {
|
||||
BellDot,
|
||||
BellOff,
|
||||
BellRing,
|
||||
Binary,
|
||||
Blocks,
|
||||
Bolt,
|
||||
Book,
|
||||
@@ -119,6 +120,7 @@ pub enum IconName {
|
||||
FileToml,
|
||||
FileTree,
|
||||
Filter,
|
||||
Flame,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
@@ -126,6 +128,7 @@ pub enum IconName {
|
||||
FontSize,
|
||||
FontWeight,
|
||||
ForwardArrow,
|
||||
Function,
|
||||
GenericClose,
|
||||
GenericMaximize,
|
||||
GenericMinimize,
|
||||
|
||||
@@ -16,7 +16,10 @@ test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
gpui.workspace = true
|
||||
release_channel.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use gpui::{AsyncApp, actions};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use client::ZED_URL_SCHEME;
|
||||
use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
|
||||
use release_channel::ReleaseChannel;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
||||
use workspace::{Toast, Workspace};
|
||||
|
||||
actions!(cli, [Install, RegisterZedScheme]);
|
||||
|
||||
pub async fn install_cli(cx: &AsyncApp) -> Result<PathBuf> {
|
||||
async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
|
||||
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
|
||||
let link_path = Path::new("/usr/local/bin/zed");
|
||||
let bin_dir_path = link_path.parent().unwrap();
|
||||
@@ -56,3 +61,47 @@ pub async fn install_cli(cx: &AsyncApp) -> Result<PathBuf> {
|
||||
Err(anyhow!("error running osascript"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
|
||||
cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"CLI should already be installed",
|
||||
Some(LINUX_PROMPT_DETAIL),
|
||||
&["Ok"],
|
||||
);
|
||||
cx.background_spawn(prompt).detach();
|
||||
return Ok(());
|
||||
}
|
||||
let path = install_script(cx.deref())
|
||||
.await
|
||||
.context("error creating CLI symlink")?;
|
||||
|
||||
workspace.update_in(cx, |workspace, _, cx| {
|
||||
struct InstalledZedCli;
|
||||
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<InstalledZedCli>(),
|
||||
format!(
|
||||
"Installed `zed` to {}. You can launch {} from your terminal.",
|
||||
path.to_string_lossy(),
|
||||
ReleaseChannel::global(cx).display_name()
|
||||
),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
register_zed_scheme(&cx).await.log_err();
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
|
||||
}
|
||||
|
||||
@@ -516,6 +516,14 @@ const RUST_BIN_NAME_TASK_VARIABLE: VariableName =
|
||||
const RUST_BIN_KIND_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND"));
|
||||
|
||||
/// The flag to list required features for executing a bin, if any
|
||||
const RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES_FLAG"));
|
||||
|
||||
/// The list of required features for executing a bin, if any
|
||||
const RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("RUST_BIN_REQUIRED_FEATURES"));
|
||||
|
||||
const RUST_TEST_FRAGMENT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("RUST_TEST_FRAGMENT"));
|
||||
|
||||
@@ -544,8 +552,8 @@ impl ContextProvider for RustContextProvider {
|
||||
|
||||
let mut variables = TaskVariables::default();
|
||||
|
||||
if let Some(target) = local_abs_path
|
||||
.and_then(|path| package_name_and_bin_name_from_abs_path(path, project_env.as_ref()))
|
||||
if let Some(target) =
|
||||
local_abs_path.and_then(|path| target_info_from_abs_path(path, project_env.as_ref()))
|
||||
{
|
||||
variables.extend(TaskVariables::from_iter([
|
||||
(RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name),
|
||||
@@ -555,6 +563,19 @@ impl ContextProvider for RustContextProvider {
|
||||
target.target_kind.to_string(),
|
||||
),
|
||||
]));
|
||||
if target.required_features.is_empty() {
|
||||
variables.insert(RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE, "".into());
|
||||
variables.insert(RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE, "".into());
|
||||
} else {
|
||||
variables.insert(
|
||||
RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.clone(),
|
||||
"--features".to_string(),
|
||||
);
|
||||
variables.insert(
|
||||
RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.clone(),
|
||||
target.required_features.join(","),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(package_name) = local_abs_path
|
||||
@@ -730,6 +751,8 @@ impl ContextProvider for RustContextProvider {
|
||||
RUST_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()),
|
||||
RUST_BIN_NAME_TASK_VARIABLE.template_value(),
|
||||
RUST_BIN_REQUIRED_FEATURES_FLAG_TASK_VARIABLE.template_value(),
|
||||
RUST_BIN_REQUIRED_FEATURES_TASK_VARIABLE.template_value(),
|
||||
],
|
||||
cwd: Some("$ZED_DIRNAME".to_owned()),
|
||||
tags: vec!["rust-main".to_owned()],
|
||||
@@ -827,6 +850,8 @@ struct CargoTarget {
|
||||
name: String,
|
||||
kind: Vec<String>,
|
||||
src_path: String,
|
||||
#[serde(rename = "required-features", default)]
|
||||
required_features: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -855,13 +880,15 @@ impl TryFrom<&str> for TargetKind {
|
||||
}
|
||||
}
|
||||
/// Which package and binary target are we in?
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct TargetInfo {
|
||||
package_name: String,
|
||||
target_name: String,
|
||||
target_kind: TargetKind,
|
||||
required_features: Vec<String>,
|
||||
}
|
||||
|
||||
fn package_name_and_bin_name_from_abs_path(
|
||||
fn target_info_from_abs_path(
|
||||
abs_path: &Path,
|
||||
project_env: Option<&HashMap<String, String>>,
|
||||
) -> Option<TargetInfo> {
|
||||
@@ -881,23 +908,10 @@ fn package_name_and_bin_name_from_abs_path(
|
||||
|
||||
let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?;
|
||||
|
||||
retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then(
|
||||
|(package_id, bin_name, target_kind)| {
|
||||
let package_name = package_name_from_pkgid(&package_id);
|
||||
|
||||
package_name.map(|package_name| TargetInfo {
|
||||
package_name: package_name.to_owned(),
|
||||
target_name: bin_name,
|
||||
target_kind,
|
||||
})
|
||||
},
|
||||
)
|
||||
target_info_from_metadata(metadata, abs_path)
|
||||
}
|
||||
|
||||
fn retrieve_package_id_and_bin_name_from_metadata(
|
||||
metadata: CargoMetadata,
|
||||
abs_path: &Path,
|
||||
) -> Option<(String, String, TargetKind)> {
|
||||
fn target_info_from_metadata(metadata: CargoMetadata, abs_path: &Path) -> Option<TargetInfo> {
|
||||
for package in metadata.packages {
|
||||
for target in package.targets {
|
||||
let Some(bin_kind) = target
|
||||
@@ -909,7 +923,12 @@ fn retrieve_package_id_and_bin_name_from_metadata(
|
||||
};
|
||||
let target_path = PathBuf::from(target.src_path);
|
||||
if target_path == abs_path {
|
||||
return Some((package.id, target.name, bin_kind));
|
||||
return package_name_from_pkgid(&package.id).map(|package_name| TargetInfo {
|
||||
package_name: package_name.to_owned(),
|
||||
target_name: target.name,
|
||||
required_features: target.required_features,
|
||||
target_kind: bin_kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1321,34 +1340,57 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retrieve_package_id_and_bin_name_from_metadata() {
|
||||
fn test_target_info_from_metadata() {
|
||||
for (input, absolute_path, expected) in [
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
|
||||
r#"{"packages":[{"id":"path+file:///absolute/path/to/project/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#,
|
||||
"/path/to/zed/src/main.rs",
|
||||
Some((
|
||||
"path+file:///path/to/zed/crates/zed#0.131.0",
|
||||
"zed",
|
||||
TargetKind::Bin,
|
||||
)),
|
||||
Some(TargetInfo {
|
||||
package_name: "zed".into(),
|
||||
target_name: "zed".into(),
|
||||
required_features: Vec::new(),
|
||||
target_kind: TargetKind::Bin,
|
||||
}),
|
||||
),
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
|
||||
"/path/to/custom-package/src/main.rs",
|
||||
Some((
|
||||
"path+file:///path/to/custom-package#my-custom-package@0.1.0",
|
||||
"my-custom-bin",
|
||||
TargetKind::Bin,
|
||||
)),
|
||||
Some(TargetInfo {
|
||||
package_name: "my-custom-package".into(),
|
||||
target_name: "my-custom-bin".into(),
|
||||
required_features: Vec::new(),
|
||||
target_kind: TargetKind::Bin,
|
||||
}),
|
||||
),
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
|
||||
"/path/to/custom-package/src/main.rs",
|
||||
Some((
|
||||
"path+file:///path/to/custom-package#my-custom-package@0.1.0",
|
||||
"my-custom-bin",
|
||||
TargetKind::Example,
|
||||
)),
|
||||
Some(TargetInfo {
|
||||
package_name: "my-custom-package".into(),
|
||||
target_name: "my-custom-bin".into(),
|
||||
required_features: Vec::new(),
|
||||
target_kind: TargetKind::Example,
|
||||
}),
|
||||
),
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs","required-features":["foo","bar"]}]}]}"#,
|
||||
"/path/to/custom-package/src/main.rs",
|
||||
Some(TargetInfo {
|
||||
package_name: "my-custom-package".into(),
|
||||
target_name: "my-custom-bin".into(),
|
||||
required_features: vec!["foo".to_owned(), "bar".to_owned()],
|
||||
target_kind: TargetKind::Example,
|
||||
}),
|
||||
),
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs","required-features":[]}]}]}"#,
|
||||
"/path/to/custom-package/src/main.rs",
|
||||
Some(TargetInfo {
|
||||
package_name: "my-custom-package".into(),
|
||||
target_name: "my-custom-bin".into(),
|
||||
required_features: vec![],
|
||||
target_kind: TargetKind::Example,
|
||||
}),
|
||||
),
|
||||
(
|
||||
r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-package","kind":["lib"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#,
|
||||
@@ -1360,10 +1402,7 @@ mod tests {
|
||||
|
||||
let absolute_path = Path::new(absolute_path);
|
||||
|
||||
assert_eq!(
|
||||
retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path),
|
||||
expected.map(|(pkgid, name, kind)| (pkgid.to_owned(), name.to_owned(), kind))
|
||||
);
|
||||
assert_eq!(target_info_from_metadata(metadata, absolute_path), expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,12 +88,21 @@ struct Options {
|
||||
}
|
||||
|
||||
pub enum CodeBlockRenderer {
|
||||
Default { copy_button: bool },
|
||||
Custom { render: CodeBlockRenderFn },
|
||||
Default {
|
||||
copy_button: bool,
|
||||
},
|
||||
Custom {
|
||||
render: CodeBlockRenderFn,
|
||||
/// A function that can modify the parent container after the code block
|
||||
/// content has been appended as a child element.
|
||||
transform: Option<CodeBlockTransformFn>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type CodeBlockRenderFn =
|
||||
Arc<dyn Fn(usize, &CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
|
||||
Arc<dyn Fn(&CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
|
||||
|
||||
pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, &mut Window, &App) -> AnyDiv>;
|
||||
|
||||
actions!(markdown, [Copy, CopyAsMarkdown]);
|
||||
|
||||
@@ -594,7 +603,7 @@ impl Element for MarkdownElement {
|
||||
0
|
||||
};
|
||||
|
||||
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
||||
for (range, event) in parsed_markdown.events.iter() {
|
||||
match event {
|
||||
MarkdownEvent::Start(tag) => {
|
||||
match tag {
|
||||
@@ -676,15 +685,9 @@ impl Element for MarkdownElement {
|
||||
builder.push_code_block(language);
|
||||
builder.push_div(code_block, range, markdown_end);
|
||||
}
|
||||
(CodeBlockRenderer::Custom { render }, _) => {
|
||||
let parent_container = render(
|
||||
index,
|
||||
kind,
|
||||
&parsed_markdown,
|
||||
range.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
(CodeBlockRenderer::Custom { render, .. }, _) => {
|
||||
let parent_container =
|
||||
render(kind, &parsed_markdown, range.clone(), window, cx);
|
||||
|
||||
builder.push_div(parent_container, range, markdown_end);
|
||||
|
||||
@@ -695,9 +698,12 @@ impl Element for MarkdownElement {
|
||||
if self.style.code_block_overflow_x_scroll {
|
||||
code_block.style().restrict_scroll_to_axis =
|
||||
Some(true);
|
||||
code_block.flex().overflow_x_scroll()
|
||||
code_block
|
||||
.flex()
|
||||
.overflow_x_scroll()
|
||||
.overflow_y_hidden()
|
||||
} else {
|
||||
code_block.w_full()
|
||||
code_block.w_full().overflow_hidden()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -846,6 +852,14 @@ impl Element for MarkdownElement {
|
||||
builder.pop_text_style();
|
||||
}
|
||||
|
||||
if let CodeBlockRenderer::Custom {
|
||||
transform: Some(modify),
|
||||
..
|
||||
} = &self.code_block_renderer
|
||||
{
|
||||
builder.modify_current_div(|el| modify(el, range.clone(), window, cx));
|
||||
}
|
||||
|
||||
if matches!(
|
||||
&self.code_block_renderer,
|
||||
CodeBlockRenderer::Default { copy_button: true }
|
||||
@@ -1049,7 +1063,7 @@ impl IntoElement for MarkdownElement {
|
||||
}
|
||||
}
|
||||
|
||||
enum AnyDiv {
|
||||
pub enum AnyDiv {
|
||||
Div(Div),
|
||||
Stateful(Stateful<Div>),
|
||||
}
|
||||
|
||||
@@ -37,3 +37,9 @@ pub(crate) mod m_2025_03_29 {
|
||||
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
pub(crate) mod m_2025_04_15 {
|
||||
mod settings;
|
||||
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
29
crates/migrator/src/migrations/m_2025_04_15/settings.rs
Normal file
29
crates/migrator/src/migrations/m_2025_04_15/settings.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::ops::Range;
|
||||
use tree_sitter::{Query, QueryMatch};
|
||||
|
||||
use crate::MigrationPatterns;
|
||||
use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN;
|
||||
|
||||
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
|
||||
SETTINGS_ASSISTANT_TOOLS_PATTERN,
|
||||
replace_bash_with_terminal_in_profiles,
|
||||
)];
|
||||
|
||||
fn replace_bash_with_terminal_in_profiles(
|
||||
contents: &str,
|
||||
mat: &QueryMatch,
|
||||
query: &Query,
|
||||
) -> Option<(Range<usize>, String)> {
|
||||
let tool_name_capture_ix = query.capture_index_for_name("tool_name")?;
|
||||
let tool_name_range = mat
|
||||
.nodes_for_capture_index(tool_name_capture_ix)
|
||||
.next()?
|
||||
.byte_range();
|
||||
let tool_name = contents.get(tool_name_range.clone())?;
|
||||
|
||||
if tool_name != "bash" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((tool_name_range, "terminal".to_string()))
|
||||
}
|
||||
@@ -120,6 +120,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
|
||||
migrations::m_2025_03_29::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_03_29,
|
||||
),
|
||||
(
|
||||
migrations::m_2025_04_15::SETTINGS_PATTERNS,
|
||||
&SETTINGS_QUERY_2025_04_15,
|
||||
),
|
||||
];
|
||||
run_migrations(text, migrations)
|
||||
}
|
||||
@@ -190,6 +194,10 @@ define_query!(
|
||||
SETTINGS_QUERY_2025_03_29,
|
||||
migrations::m_2025_03_29::SETTINGS_PATTERNS
|
||||
);
|
||||
define_query!(
|
||||
SETTINGS_QUERY_2025_04_15,
|
||||
migrations::m_2025_04_15::SETTINGS_PATTERNS
|
||||
);
|
||||
|
||||
// custom query
|
||||
static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
@@ -527,4 +535,103 @@ mod tests {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_bash_with_terminal_in_profiles() {
|
||||
assert_migrate_settings(
|
||||
r#"
|
||||
{
|
||||
"assistant": {
|
||||
"profiles": {
|
||||
"custom": {
|
||||
"name": "Custom",
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#,
|
||||
Some(
|
||||
r#"
|
||||
{
|
||||
"assistant": {
|
||||
"profiles": {
|
||||
"custom": {
|
||||
"name": "Custom",
|
||||
"tools": {
|
||||
"terminal": true,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_bash_false_with_terminal_in_profiles() {
|
||||
assert_migrate_settings(
|
||||
r#"
|
||||
{
|
||||
"assistant": {
|
||||
"profiles": {
|
||||
"custom": {
|
||||
"name": "Custom",
|
||||
"tools": {
|
||||
"bash": false,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#,
|
||||
Some(
|
||||
r#"
|
||||
{
|
||||
"assistant": {
|
||||
"profiles": {
|
||||
"custom": {
|
||||
"name": "Custom",
|
||||
"tools": {
|
||||
"terminal": false,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_bash_in_profiles() {
|
||||
assert_migrate_settings(
|
||||
r#"
|
||||
{
|
||||
"assistant": {
|
||||
"profiles": {
|
||||
"custom": {
|
||||
"name": "Custom",
|
||||
"tools": {
|
||||
"diagnostics": true,
|
||||
"path_search": true,
|
||||
"read_file": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ pub(crate) use keymap::{
|
||||
};
|
||||
|
||||
pub(crate) use settings::{
|
||||
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
|
||||
SETTINGS_ASSISTANT_TOOLS_PATTERN, SETTINGS_LANGUAGES_PATTERN,
|
||||
SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
|
||||
};
|
||||
|
||||
@@ -39,3 +39,35 @@ pub const SETTINGS_LANGUAGES_PATTERN: &str = r#"(document
|
||||
)
|
||||
(#eq? @languages "languages")
|
||||
)"#;
|
||||
|
||||
pub const SETTINGS_ASSISTANT_TOOLS_PATTERN: &str = r#"(document
|
||||
(object
|
||||
(pair
|
||||
key: (string (string_content) @assistant)
|
||||
value: (object
|
||||
(pair
|
||||
key: (string (string_content) @profiles)
|
||||
value: (object
|
||||
(pair
|
||||
key: (_)
|
||||
value: (object
|
||||
(pair
|
||||
key: (string (string_content) @tools_key)
|
||||
value: (object
|
||||
(pair
|
||||
key: (string (string_content) @tool_name)
|
||||
value: (_) @tool_value
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(#eq? @assistant "assistant")
|
||||
(#eq? @profiles "profiles")
|
||||
(#eq? @tools_key "tools")
|
||||
)"#;
|
||||
|
||||
@@ -1118,9 +1118,16 @@ impl MultiBuffer {
|
||||
self.history.start_transaction(now)
|
||||
}
|
||||
|
||||
pub fn last_transaction_id(&self) -> Option<TransactionId> {
|
||||
let last_transaction = self.history.undo_stack.last()?;
|
||||
return Some(last_transaction.id);
|
||||
pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
return buffer.read_with(cx, |b, _| {
|
||||
b.peek_undo_stack()
|
||||
.map(|history_entry| history_entry.transaction_id())
|
||||
});
|
||||
} else {
|
||||
let last_transaction = self.history.undo_stack.last()?;
|
||||
return Some(last_transaction.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
|
||||
@@ -12,7 +12,7 @@ use rpc::{
|
||||
proto::{self},
|
||||
};
|
||||
use std::{hash::Hash, ops::Range, path::Path, sync::Arc};
|
||||
use text::PointUtf16;
|
||||
use text::{Point, PointUtf16};
|
||||
|
||||
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
|
||||
|
||||
@@ -464,6 +464,23 @@ impl BreakpointStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn breakpoint_at_row(
|
||||
&self,
|
||||
path: &Path,
|
||||
row: u32,
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
|
||||
self.breakpoints.get(path).and_then(|breakpoints| {
|
||||
let snapshot = breakpoints.buffer.read(cx).text_snapshot();
|
||||
|
||||
breakpoints
|
||||
.breakpoints
|
||||
.iter()
|
||||
.find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
|
||||
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
|
||||
self.breakpoints
|
||||
.get(path)
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{Ok, Result, anyhow};
|
||||
use dap::{
|
||||
Capabilities, ContinueArguments, InitializeRequestArguments,
|
||||
Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments,
|
||||
InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint,
|
||||
StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable,
|
||||
VariablesArgumentsFilter,
|
||||
@@ -1665,6 +1665,44 @@ impl LocalDapCommand for SetBreakpoints {
|
||||
Ok(message.breakpoints)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||
pub(super) enum SetExceptionBreakpoints {
|
||||
Plain {
|
||||
filters: Vec<String>,
|
||||
},
|
||||
WithOptions {
|
||||
filters: Vec<ExceptionFilterOptions>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LocalDapCommand for SetExceptionBreakpoints {
|
||||
type Response = Vec<dap::Breakpoint>;
|
||||
type DapRequest = dap::requests::SetExceptionBreakpoints;
|
||||
|
||||
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
|
||||
match self {
|
||||
SetExceptionBreakpoints::Plain { filters } => dap::SetExceptionBreakpointsArguments {
|
||||
filters: filters.clone(),
|
||||
exception_options: None,
|
||||
filter_options: None,
|
||||
},
|
||||
SetExceptionBreakpoints::WithOptions { filters } => {
|
||||
dap::SetExceptionBreakpointsArguments {
|
||||
filters: vec![],
|
||||
filter_options: Some(filters.clone()),
|
||||
exception_options: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn response_from_dap(
|
||||
&self,
|
||||
message: <Self::DapRequest as dap::requests::Request>::Response,
|
||||
) -> Result<Self::Response> {
|
||||
Ok(message.breakpoints.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub(super) struct LocationsCommand {
|
||||
|
||||
@@ -852,8 +852,7 @@ fn create_new_session(
|
||||
cx.emit(DapStoreEvent::DebugClientStarted(session_id));
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
match {
|
||||
let seq_result = {
|
||||
session
|
||||
.update(cx, |session, cx| session.request_initialize(cx))?
|
||||
.await?;
|
||||
@@ -863,7 +862,8 @@ fn create_new_session(
|
||||
session.initialize_sequence(initialized_rx, cx)
|
||||
})?
|
||||
.await
|
||||
} {
|
||||
};
|
||||
match seq_result {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
this.update(cx, |this, cx| {
|
||||
|
||||
@@ -7,9 +7,9 @@ use super::dap_command::{
|
||||
self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand,
|
||||
EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
|
||||
ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand,
|
||||
ScopesCommand, SetVariableValueCommand, StackTraceCommand, StepBackCommand, StepCommand,
|
||||
StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, ThreadsCommand,
|
||||
VariablesCommand,
|
||||
ScopesCommand, SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand,
|
||||
StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand,
|
||||
TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
|
||||
};
|
||||
use super::dap_store::DapAdapterDelegate;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -23,7 +23,10 @@ use dap::{
|
||||
client::{DebugAdapterClient, SessionId},
|
||||
messages::{Events, Message},
|
||||
};
|
||||
use dap::{DapRegistry, DebugRequestType, OutputEventCategory};
|
||||
use dap::{
|
||||
DapRegistry, DebugRequestType, ExceptionBreakpointsFilter, ExceptionFilterOptions,
|
||||
OutputEventCategory,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{
|
||||
@@ -34,6 +37,7 @@ use serde_json::{Value, json};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::any::TypeId;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::u64;
|
||||
use std::{
|
||||
@@ -324,6 +328,13 @@ impl LocalMode {
|
||||
}
|
||||
}
|
||||
|
||||
session
|
||||
.client
|
||||
.on_request::<dap::requests::SetExceptionBreakpoints, _>(move |_, _| {
|
||||
Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None })
|
||||
})
|
||||
.await;
|
||||
|
||||
session
|
||||
.client
|
||||
.on_request::<dap::requests::Disconnect, _>(move |_, _| Ok(()))
|
||||
@@ -456,7 +467,31 @@ impl LocalMode {
|
||||
})
|
||||
}
|
||||
|
||||
fn send_all_breakpoints(&self, ignore_breakpoints: bool, cx: &App) -> Task<()> {
|
||||
fn send_exception_breakpoints(
|
||||
&self,
|
||||
filters: Vec<ExceptionBreakpointsFilter>,
|
||||
supports_filter_options: bool,
|
||||
cx: &App,
|
||||
) -> Task<Result<Vec<dap::Breakpoint>>> {
|
||||
let arg = if supports_filter_options {
|
||||
SetExceptionBreakpoints::WithOptions {
|
||||
filters: filters
|
||||
.into_iter()
|
||||
.map(|filter| ExceptionFilterOptions {
|
||||
filter_id: filter.filter,
|
||||
condition: None,
|
||||
mode: None,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
} else {
|
||||
SetExceptionBreakpoints::Plain {
|
||||
filters: filters.into_iter().map(|filter| filter.filter).collect(),
|
||||
}
|
||||
};
|
||||
self.request(arg, cx.background_executor().clone())
|
||||
}
|
||||
fn send_source_breakpoints(&self, ignore_breakpoints: bool, cx: &App) -> Task<()> {
|
||||
let mut breakpoint_tasks = Vec::new();
|
||||
let breakpoints = self
|
||||
.breakpoint_store
|
||||
@@ -588,15 +623,37 @@ impl LocalMode {
|
||||
};
|
||||
|
||||
let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
|
||||
|
||||
let exception_filters = capabilities
|
||||
.exception_breakpoint_filters
|
||||
.as_ref()
|
||||
.map(|exception_filters| {
|
||||
exception_filters
|
||||
.iter()
|
||||
.filter(|filter| filter.default == Some(true))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let supports_exception_filters = capabilities
|
||||
.supports_exception_filter_options
|
||||
.unwrap_or_default();
|
||||
let configuration_sequence = cx.spawn({
|
||||
let this = self.clone();
|
||||
async move |cx| {
|
||||
initialized_rx.await?;
|
||||
// todo(debugger) figure out if we want to handle a breakpoint response error
|
||||
// This will probably consist of letting a user know that breakpoints failed to be set
|
||||
cx.update(|cx| this.send_all_breakpoints(false, cx))?.await;
|
||||
|
||||
cx.update(|cx| this.send_source_breakpoints(false, cx))?
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
this.send_exception_breakpoints(
|
||||
exception_filters,
|
||||
supports_exception_filters,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.ok();
|
||||
if configuration_done_supported {
|
||||
this.request(ConfigurationDone {}, cx.background_executor().clone())
|
||||
} else {
|
||||
@@ -727,6 +784,8 @@ impl ThreadStates {
|
||||
}
|
||||
const MAX_TRACKED_OUTPUT_EVENTS: usize = 5000;
|
||||
|
||||
type IsEnabled = bool;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct OutputToken(pub usize);
|
||||
/// Represents a current state of a single debug adapter and provides ways to mutate it.
|
||||
@@ -748,6 +807,7 @@ pub struct Session {
|
||||
locations: HashMap<u64, dap::LocationsResponse>,
|
||||
is_session_terminated: bool,
|
||||
requests: HashMap<TypeId, HashMap<RequestSlot, Shared<Task<Option<()>>>>>,
|
||||
exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
|
||||
_background_tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -956,6 +1016,7 @@ impl Session {
|
||||
_background_tasks: Vec::default(),
|
||||
locations: Default::default(),
|
||||
is_session_terminated: false,
|
||||
exception_breakpoints: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1022,6 +1083,18 @@ impl Session {
|
||||
let capabilities = capabilities.await?;
|
||||
this.update(cx, |session, _| {
|
||||
session.capabilities = capabilities;
|
||||
let filters = session
|
||||
.capabilities
|
||||
.exception_breakpoint_filters
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
for filter in filters {
|
||||
let default = filter.default.unwrap_or_default();
|
||||
session
|
||||
.exception_breakpoints
|
||||
.entry(filter.filter.clone())
|
||||
.or_insert_with(|| (filter, default));
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -1464,13 +1537,46 @@ impl Session {
|
||||
self.ignore_breakpoints = ignore;
|
||||
|
||||
if let Some(local) = self.as_local() {
|
||||
local.send_all_breakpoints(ignore, cx)
|
||||
local.send_source_breakpoints(ignore, cx)
|
||||
} else {
|
||||
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exception_breakpoints(
|
||||
&self,
|
||||
) -> impl Iterator<Item = &(ExceptionBreakpointsFilter, IsEnabled)> {
|
||||
self.exception_breakpoints.values()
|
||||
}
|
||||
|
||||
pub fn toggle_exception_breakpoint(&mut self, id: &str, cx: &App) {
|
||||
if let Some((_, is_enabled)) = self.exception_breakpoints.get_mut(id) {
|
||||
*is_enabled = !*is_enabled;
|
||||
self.send_exception_breakpoints(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_exception_breakpoints(&mut self, cx: &App) {
|
||||
if let Some(local) = self.as_local() {
|
||||
let exception_filters = self
|
||||
.exception_breakpoints
|
||||
.values()
|
||||
.filter_map(|(filter, is_enabled)| is_enabled.then(|| filter.clone()))
|
||||
.collect();
|
||||
|
||||
let supports_exception_filters = self
|
||||
.capabilities
|
||||
.supports_exception_filter_options
|
||||
.unwrap_or_default();
|
||||
local
|
||||
.send_exception_breakpoints(exception_filters, supports_exception_filters, cx)
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
debug_assert!(false, "Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn breakpoints_enabled(&self) -> bool {
|
||||
self.ignore_breakpoints
|
||||
}
|
||||
@@ -2084,6 +2190,7 @@ fn create_local_session(
|
||||
threads: IndexMap::default(),
|
||||
stack_frames: IndexMap::default(),
|
||||
locations: Default::default(),
|
||||
exception_breakpoints: Default::default(),
|
||||
_background_tasks,
|
||||
is_session_terminated: false,
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ impl ProjectEnvironment {
|
||||
}
|
||||
|
||||
if let Some(cli_environment) = self.get_cli_environment() {
|
||||
log::info!("using project environment variables from CLI");
|
||||
log::debug!("using project environment variables from CLI");
|
||||
return Task::ready(Some(cli_environment)).shared();
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ impl ProjectEnvironment {
|
||||
}
|
||||
|
||||
if let Some(cli_environment) = self.get_cli_environment() {
|
||||
log::info!("using project environment variables from CLI");
|
||||
log::debug!("using project environment variables from CLI");
|
||||
return Task::ready(Some(cli_environment)).shared();
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ impl ProjectEnvironment {
|
||||
}
|
||||
|
||||
if let Some(cli_environment) = self.get_cli_environment() {
|
||||
log::info!("using project environment variables from CLI");
|
||||
log::debug!("using project environment variables from CLI");
|
||||
return Task::ready(Some(cli_environment)).shared();
|
||||
}
|
||||
|
||||
|
||||
@@ -2630,9 +2630,7 @@ impl RepositorySnapshot {
|
||||
}
|
||||
|
||||
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
|
||||
self.statuses_by_path
|
||||
.get(&PathKey(repo_path.0.clone()), &())
|
||||
.map_or(false, |entry| entry.status.is_conflicted())
|
||||
self.merge_conflicts.contains(repo_path)
|
||||
}
|
||||
|
||||
/// This is the name that will be displayed in the repository selector for this repository.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -334,6 +334,10 @@ impl ProjectPath {
|
||||
path: Path::new("").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: &ProjectPath) -> bool {
|
||||
self.worktree_id == other.worktree_id && self.path.starts_with(&other.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -71,6 +71,7 @@ pub enum SearchQuery {
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
one_match_per_line: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
}
|
||||
@@ -116,6 +117,7 @@ impl SearchQuery {
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
one_match_per_line: bool,
|
||||
files_to_include: PathMatcher,
|
||||
files_to_exclude: PathMatcher,
|
||||
buffers: Option<Vec<Entity<Buffer>>>,
|
||||
@@ -156,6 +158,7 @@ impl SearchQuery {
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
one_match_per_line,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +169,7 @@ impl SearchQuery {
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
false,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
None, // search opened only don't need search remote
|
||||
@@ -459,6 +463,19 @@ impl SearchQuery {
|
||||
Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this search should replace only one match per line, instead of
|
||||
/// all matches.
|
||||
/// Returns `None` for text searches, as only regex searches support this
|
||||
/// option.
|
||||
pub fn one_match_per_line(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::Regex {
|
||||
one_match_per_line, ..
|
||||
} => Some(*one_match_per_line),
|
||||
Self::Text { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<PathMatcher> {
|
||||
|
||||
@@ -2736,6 +2736,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
};
|
||||
cx.run_until_parked();
|
||||
cx.simulate_modifiers_change(modifiers_with_shift);
|
||||
cx.update(|window, cx| {
|
||||
panel.update(cx, |this, cx| {
|
||||
|
||||
@@ -14,12 +14,15 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, get_system_shell};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AssistantSystemPromptContext {
|
||||
pub worktrees: Vec<WorktreeInfoForSystemPrompt>,
|
||||
pub has_rules: bool,
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
impl AssistantSystemPromptContext {
|
||||
@@ -30,6 +33,9 @@ impl AssistantSystemPromptContext {
|
||||
Self {
|
||||
worktrees,
|
||||
has_rules,
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: get_system_shell(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,12 +267,6 @@ impl PromptBuilder {
|
||||
.render("assistant_system_prompt", context)
|
||||
}
|
||||
|
||||
pub fn generate_assistant_system_prompt_reminder(&self) -> Result<String, RenderError> {
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("assistant_system_prompt_reminder", &())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
|
||||
@@ -987,6 +987,16 @@ impl BufferSearchBar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_search_within_ranges(
|
||||
&mut self,
|
||||
search_options: SearchOptions,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.search_options = search_options;
|
||||
self.adjust_query_regex_language(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next_match(
|
||||
&mut self,
|
||||
_: &SelectNextMatch,
|
||||
@@ -1231,6 +1241,8 @@ impl BufferSearchBar {
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
false,
|
||||
self.search_options
|
||||
.contains(SearchOptions::ONE_MATCH_PER_LINE),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
|
||||
@@ -1053,6 +1053,8 @@ impl ProjectSearchView {
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
self.search_options
|
||||
.contains(SearchOptions::ONE_MATCH_PER_LINE),
|
||||
included_files,
|
||||
excluded_files,
|
||||
open_buffers,
|
||||
|
||||
@@ -48,6 +48,7 @@ bitflags! {
|
||||
const CASE_SENSITIVE = 0b010;
|
||||
const INCLUDE_IGNORED = 0b100;
|
||||
const REGEX = 0b1000;
|
||||
const ONE_MATCH_PER_LINE = 0b100000;
|
||||
/// If set, reverse direction when finding the active match
|
||||
const BACKWARDS = 0b10000;
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ impl ShellBuilder {
|
||||
|
||||
// `alacritty_terminal` uses this as default on Windows. See:
|
||||
// https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
|
||||
// We could use `util::retrieve_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
|
||||
// We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
|
||||
// should be okay.
|
||||
fn system_shell() -> String {
|
||||
"powershell.exe".to_string()
|
||||
|
||||
@@ -380,7 +380,7 @@ impl TerminalBuilder {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Some(alacritty_terminal::tty::Shell::new(
|
||||
util::retrieve_system_shell(),
|
||||
util::get_windows_system_shell(),
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ mod notification;
|
||||
mod numeric_stepper;
|
||||
mod popover;
|
||||
mod popover_menu;
|
||||
mod progress;
|
||||
mod radio;
|
||||
mod right_click_menu;
|
||||
mod scrollbar;
|
||||
@@ -61,6 +62,7 @@ pub use notification::*;
|
||||
pub use numeric_stepper::*;
|
||||
pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
pub use progress::*;
|
||||
pub use radio::*;
|
||||
pub use right_click_menu::*;
|
||||
pub use scrollbar::*;
|
||||
|
||||
@@ -267,7 +267,7 @@ impl RenderOnce for IconWithIndicator {
|
||||
|
||||
impl Component for Icon {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Images
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
|
||||
|
||||
impl Component for DecoratedIcon {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Images
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
||||
@@ -199,7 +199,7 @@ impl RenderOnce for Label {
|
||||
|
||||
impl Component for Label {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Typography
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
||||
2
crates/ui/src/components/progress.rs
Normal file
2
crates/ui/src/components/progress.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod progress_bar;
|
||||
pub use progress_bar::*;
|
||||
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use documented::Documented;
|
||||
use gpui::{Hsla, point};
|
||||
|
||||
use crate::components::Label;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A progress bar is a horizontal bar that communicates the status of a process.
|
||||
///
|
||||
/// A progress bar should not be used to represent indeterminate progress.
|
||||
#[derive(RegisterComponent, Documented)]
|
||||
pub struct ProgressBar {
|
||||
id: ElementId,
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
bg_color: Hsla,
|
||||
fg_color: Hsla,
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
/// Create a new progress bar with the given value and maximum value.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
value,
|
||||
max_value,
|
||||
bg_color: cx.theme().colors().background,
|
||||
fg_color: cx.theme().status().info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current value of the progress bar.
|
||||
pub fn value(&mut self, value: f32) -> &mut Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum value of the progress bar.
|
||||
pub fn max_value(&mut self, max_value: f32) -> &mut Self {
|
||||
self.max_value = max_value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the background color of the progress bar.
|
||||
pub fn bg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the foreground color of the progress bar.
|
||||
pub fn fg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
self.fg_color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProgressBar {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let fill_width = (self.value / self.max_value).clamp(0.02, 1.0);
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.h(px(8.0))
|
||||
.rounded_full()
|
||||
.py(px(2.0))
|
||||
.px(px(4.0))
|
||||
.bg(self.bg_color)
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.08),
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.rounded_full()
|
||||
.bg(self.fg_color)
|
||||
.w(relative(fill_width)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ProgressBar {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Status
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some(Self::DOCS)
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let max_value = 180.0;
|
||||
|
||||
let empty_progress_bar = cx.new(|cx| ProgressBar::new("empty", 0.0, max_value, cx));
|
||||
let partial_progress_bar =
|
||||
cx.new(|cx| ProgressBar::new("partial", max_value * 0.35, max_value, cx));
|
||||
let filled_progress_bar = cx.new(|cx| ProgressBar::new("filled", max_value, max_value, cx));
|
||||
|
||||
Some(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.w(px(240.0))
|
||||
.child(div().child("Progress Bar"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("0%"))
|
||||
.child(Label::new("Empty")),
|
||||
)
|
||||
.child(empty_progress_bar.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("38%"))
|
||||
.child(Label::new("Partial")),
|
||||
)
|
||||
.child(partial_progress_bar.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("100%"))
|
||||
.child(Label::new("Complete")),
|
||||
)
|
||||
.child(filled_progress_bar.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ impl Headline {
|
||||
|
||||
impl Component for Headline {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Typography
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
||||
@@ -477,7 +477,7 @@ pub fn iterate_expanded_and_wrapped_usize_range(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn retrieve_system_shell() -> String {
|
||||
pub fn get_windows_system_shell() -> String {
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
|
||||
@@ -994,6 +994,18 @@ pub fn default<D: Default>() -> D {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn get_system_shell() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
get_windows_system_shell()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user