merge
This commit is contained in:
commit
da653aaf6f
381 changed files with 14396 additions and 7154 deletions
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
@ -30,6 +30,7 @@ jobs:
|
|||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
run_docs: ${{ steps.filter.outputs.run_docs }}
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
|
@ -69,6 +70,12 @@ jobs:
|
|||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
|
||||
echo "run_nix=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_nix=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
|
@ -746,7 +753,10 @@ jobs:
|
|||
nix-build:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries' &&
|
||||
(contains(github.event.pull_request.labels.*.name, 'run-nix') ||
|
||||
needs.job_spec.outputs.run_nix == 'true')
|
||||
secrets: inherit
|
||||
with:
|
||||
flake-output: debug
|
||||
|
|
56
Cargo.lock
generated
56
Cargo.lock
generated
|
@ -1911,7 +1911,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
|
@ -2077,7 +2076,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
|
@ -2110,7 +2109,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2120,7 +2119,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
|
@ -4133,7 +4132,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
|
@ -4148,6 +4147,8 @@ dependencies = [
|
|||
"async-trait",
|
||||
"collections",
|
||||
"dap",
|
||||
"dotenvy",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"json_dotpath",
|
||||
|
@ -4676,12 +4677,6 @@ dependencies = [
|
|||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
|
@ -4815,6 +4810,7 @@ dependencies = [
|
|||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"schemars",
|
||||
|
@ -4835,6 +4831,7 @@ dependencies = [
|
|||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-yaml",
|
||||
"ui",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
|
@ -5115,7 +5112,7 @@ dependencies = [
|
|||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"fs",
|
||||
|
@ -8848,6 +8845,7 @@ dependencies = [
|
|||
"http_client",
|
||||
"imara-diff",
|
||||
"indoc",
|
||||
"inventory",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"lsp",
|
||||
|
@ -8946,8 +8944,10 @@ dependencies = [
|
|||
"aws-credential-types",
|
||||
"aws_http_client",
|
||||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
|
@ -9024,7 +9024,6 @@ dependencies = [
|
|||
"itertools 0.14.0",
|
||||
"language",
|
||||
"lsp",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
|
@ -12260,6 +12259,7 @@ dependencies = [
|
|||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
|
@ -14054,12 +14054,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -14067,9 +14068,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -14568,16 +14569,29 @@ dependencies = [
|
|||
name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
|
@ -16011,6 +16025,7 @@ dependencies = [
|
|||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"log",
|
||||
"palette",
|
||||
"parking_lot",
|
||||
|
@ -17333,6 +17348,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
|
@ -19930,7 +19946,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.194.0"
|
||||
version = "0.195.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
@ -20128,9 +20144,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
|
||||
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
|
20
Cargo.toml
20
Cargo.toml
|
@ -425,9 +425,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
|||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
|
@ -444,12 +444,12 @@ core-video = { version = "0.4.3", features = ["metal"] }
|
|||
cpal = "0.16"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
|
@ -480,7 +480,7 @@ json_dotpath = "1.1"
|
|||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
|
@ -491,7 +491,7 @@ metal = "0.29"
|
|||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
|
@ -531,7 +531,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
|
|||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
|
@ -540,7 +540,7 @@ rustc-hash = "2.1.0"
|
|||
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"] }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
|
@ -625,7 +625,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
|||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.4"
|
||||
zed_llm_client = "= 0.8.5"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"ctrl-shift-f5": "debugger::RerunSession",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"ctrl-f11": "debugger::StepInto",
|
||||
|
@ -557,6 +557,13 @@
|
|||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-alt-0": "workspace::ResetActiveDockSize",
|
||||
// For 0px parameter, uses UI font size value.
|
||||
"ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
|
||||
"ctrl-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
|
||||
"ctrl-alt-)": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-alt-_": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-alt-+": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
|
@ -598,7 +605,9 @@
|
|||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
// or by tag:
|
||||
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
|
||||
"f5": "debugger::RerunLastSession"
|
||||
"f5": "debugger::Rerun",
|
||||
"ctrl-f4": "workspace::CloseActiveDock",
|
||||
"ctrl-w": "workspace::CloseActiveDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -701,6 +710,13 @@
|
|||
"pagedown": "editor::ContextMenuLast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"up": "editor::SignatureHelpPrevious",
|
||||
"down": "editor::SignatureHelpNext"
|
||||
}
|
||||
},
|
||||
// Custom bindings
|
||||
{
|
||||
"bindings": {
|
||||
|
@ -1067,5 +1083,19 @@
|
|||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"pageup": "markdown::MovePageUp",
|
||||
"pagedown": "markdown::MovePageDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
"bindings": {
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"shift-cmd-f5": "debugger::RerunSession",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"f11": "debugger::StepInto",
|
||||
"ctrl-f11": "debugger::StepInto",
|
||||
"shift-f11": "debugger::StepOut",
|
||||
"home": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
|
@ -624,6 +624,13 @@
|
|||
"cmd-r": "workspace::ToggleRightDock",
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
// For 0px parameter, uses UI font size value.
|
||||
"ctrl-alt-0": "workspace::ResetActiveDockSize",
|
||||
"ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
|
||||
"ctrl-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
|
||||
"ctrl-alt-)": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-alt-_": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-alt-+": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||
|
@ -652,7 +659,8 @@
|
|||
"cmd-k shift-up": "workspace::SwapPaneUp",
|
||||
"cmd-k shift-down": "workspace::SwapPaneDown",
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"f5": "debugger::RerunLastSession"
|
||||
"f5": "debugger::Rerun",
|
||||
"cmd-w": "workspace::CloseActiveDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -766,6 +774,13 @@
|
|||
"pagedown": "editor::ContextMenuLast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"up": "editor::SignatureHelpPrevious",
|
||||
"down": "editor::SignatureHelpNext"
|
||||
}
|
||||
},
|
||||
// Custom bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
|
@ -1167,5 +1182,19 @@
|
|||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"pageup": "markdown::MovePageUp",
|
||||
"pagedown": "markdown::MovePageDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -98,6 +98,13 @@
|
|||
"ctrl-n": "editor::ContextMenuNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
|
|
|
@ -98,6 +98,13 @@
|
|||
"ctrl-n": "editor::ContextMenuNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
|
|
|
@ -210,7 +210,8 @@
|
|||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile"
|
||||
"ctrl-^": "pane::AlternateFile",
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -219,7 +220,6 @@
|
|||
"ctrl-[": "editor::Cancel",
|
||||
"escape": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
".": "vim::Repeat",
|
||||
"c": "vim::PushChange",
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": "vim::PushDelete",
|
||||
|
@ -327,6 +327,7 @@
|
|||
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g w": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
|
@ -477,6 +478,13 @@
|
|||
"ctrl-n": "editor::ShowWordCompletions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == replace",
|
||||
"bindings": {
|
||||
|
@ -849,6 +857,25 @@
|
|||
"shift-u": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height && VimControl",
|
||||
"bindings": {
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && edit_prediction",
|
||||
"bindings": {
|
||||
|
@ -860,14 +887,7 @@
|
|||
{
|
||||
"context": "MessageEditor > Editor && VimControl",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
"enter": "agent::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -617,6 +617,8 @@
|
|||
// 3. Mark files with errors and warnings:
|
||||
// "all"
|
||||
"show_diagnostics": "all",
|
||||
// Whether to stick parent directories at top of the project panel.
|
||||
"sticky_scroll": true,
|
||||
// Settings related to indent guides in the project panel.
|
||||
"indent_guides": {
|
||||
// When to show indent guides in the project panel.
|
||||
|
@ -746,8 +748,6 @@
|
|||
"default_width": 380
|
||||
},
|
||||
"agent": {
|
||||
// Version of this setting.
|
||||
"version": "2",
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||
|
@ -810,6 +810,7 @@
|
|||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"move_path": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
|
@ -829,6 +830,7 @@
|
|||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
"project_notifications": true,
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
|
@ -1292,6 +1294,8 @@
|
|||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Whether to keep the text selection after copying it to the clipboard
|
||||
"keep_selection_on_copy": false,
|
||||
// Whether to show the terminal button in the status bar
|
||||
"button": true,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
|
@ -1656,7 +1660,6 @@
|
|||
// Different settings for specific language models.
|
||||
"language_models": {
|
||||
"anthropic": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.anthropic.com"
|
||||
},
|
||||
"google": {
|
||||
|
@ -1666,7 +1669,6 @@
|
|||
"api_url": "http://localhost:11434"
|
||||
},
|
||||
"openai": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"open_router": {
|
||||
|
@ -1784,7 +1786,8 @@
|
|||
// `socks5h`. `http` will be used when no scheme is specified.
|
||||
//
|
||||
// By default no proxy will be used, or Zed will try get proxy settings from
|
||||
// environment variables.
|
||||
// environment variables. If certain hosts should not be proxied,
|
||||
// set the `no_proxy` environment variable and provide a comma-separated list.
|
||||
//
|
||||
// Examples:
|
||||
// - "proxy": "socks5h://localhost:10808"
|
||||
|
|
|
@ -31,7 +31,13 @@ use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
|||
|
||||
const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
actions!(
|
||||
activity_indicator,
|
||||
[
|
||||
/// Displays error messages from language servers in the status bar.
|
||||
ShowErrorMessage
|
||||
]
|
||||
);
|
||||
|
||||
pub enum Event {
|
||||
ShowStatus {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName};
|
||||
use collections::IndexMap;
|
||||
use convert_case::{Case, Casing};
|
||||
use fs::Fs;
|
||||
|
@ -72,7 +72,7 @@ impl AgentProfile {
|
|||
&self.id
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
|
||||
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
@ -81,7 +81,7 @@ impl AgentProfile {
|
|||
.read(cx)
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.filter(|tool| Self::is_enabled(settings, tool.source(), tool.name()))
|
||||
.filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,7 @@ mod tests {
|
|||
let mut enabled_tools = cx
|
||||
.read(|cx| profile.enabled_tools(cx))
|
||||
.into_iter()
|
||||
.map(|tool| tool.name())
|
||||
.map(|(_, tool)| tool.name())
|
||||
.collect::<Vec<_>>();
|
||||
enabled_tools.sort();
|
||||
|
||||
|
@ -174,7 +174,7 @@ mod tests {
|
|||
let mut enabled_tools = cx
|
||||
.read(|cx| profile.enabled_tools(cx))
|
||||
.into_iter()
|
||||
.map(|tool| tool.name())
|
||||
.map(|(_, tool)| tool.name())
|
||||
.collect::<Vec<_>>();
|
||||
enabled_tools.sort();
|
||||
|
||||
|
@ -207,7 +207,7 @@ mod tests {
|
|||
let mut enabled_tools = cx
|
||||
.read(|cx| profile.enabled_tools(cx))
|
||||
.into_iter()
|
||||
.map(|tool| tool.name())
|
||||
.map(|(_, tool)| tool.name())
|
||||
.collect::<Vec<_>>();
|
||||
enabled_tools.sort();
|
||||
|
||||
|
@ -267,10 +267,10 @@ mod tests {
|
|||
}
|
||||
|
||||
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
|
||||
cx.new(|_| {
|
||||
cx.new(|cx| {
|
||||
let mut tool_set = ToolWorkingSet::default();
|
||||
tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")));
|
||||
tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")));
|
||||
tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx);
|
||||
tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx);
|
||||
tool_set
|
||||
})
|
||||
}
|
||||
|
|
3
crates/agent/src/prompts/stale_files_prompt_header.txt
Normal file
3
crates/agent/src/prompts/stale_files_prompt_header.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
|
@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
|
|||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||
use git::repository::DiffType;
|
||||
|
@ -23,11 +23,10 @@ use gpui::{
|
|||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
|
||||
TokenUsage,
|
||||
LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError,
|
||||
PaymentRequiredError, Role, SelectedModel, StopReason, TokenUsage,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::{
|
||||
|
@ -46,7 +45,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use util::{ResultExt as _, debug_panic, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
|
@ -961,13 +960,14 @@ impl Thread {
|
|||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<LanguageModelRequestTool> {
|
||||
if model.supports_tools() {
|
||||
resolve_tool_name_conflicts(self.profile.enabled_tools(cx).as_slice())
|
||||
self.profile
|
||||
.enabled_tools(cx)
|
||||
.into_iter()
|
||||
.filter_map(|(name, tool)| {
|
||||
// Skip tools that cannot be supported
|
||||
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name,
|
||||
name: name.into(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
|
@ -1248,6 +1248,8 @@ impl Thread {
|
|||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
self.flush_notifications(model.clone(), intent, cx);
|
||||
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
|
||||
self.stream_completion(request, model, intent, window, cx);
|
||||
|
@ -1481,6 +1483,110 @@ impl Thread {
|
|||
request
|
||||
}
|
||||
|
||||
/// Insert auto-generated notifications (if any) to the thread
|
||||
fn flush_notifications(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match intent {
|
||||
CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
|
||||
if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id: pending_tool_use.id.clone(),
|
||||
pending_tool_use: Some(pending_tool_use),
|
||||
});
|
||||
}
|
||||
}
|
||||
CompletionIntent::ThreadSummarization
|
||||
| CompletionIntent::ThreadContextSummarization
|
||||
| CompletionIntent::CreateFile
|
||||
| CompletionIntent::EditFile
|
||||
| CompletionIntent::InlineAssist
|
||||
| CompletionIntent::TerminalInlineAssist
|
||||
| CompletionIntent::GenerateGitCommitMessage => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn attach_tracked_files_state(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let action_log = self.action_log.read(cx);
|
||||
|
||||
action_log.stale_buffers(cx).next()?;
|
||||
|
||||
// Represent notification as a simulated `project_notifications` tool call
|
||||
let tool_name = Arc::from("project_notifications");
|
||||
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
|
||||
debug_panic!("`project_notifications` tool not found");
|
||||
return None;
|
||||
};
|
||||
|
||||
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input = serde_json::json!({});
|
||||
let request = Arc::new(LanguageModelRequest::default()); // unused
|
||||
let window = None;
|
||||
let tool_result = tool.run(
|
||||
input,
|
||||
request,
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
model.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let tool_use_id =
|
||||
LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
|
||||
|
||||
let tool_use = LanguageModelToolUse {
|
||||
id: tool_use_id.clone(),
|
||||
name: tool_name.clone(),
|
||||
raw_input: "{}".to_string(),
|
||||
input: serde_json::json!({}),
|
||||
is_input_complete: true,
|
||||
};
|
||||
|
||||
let tool_output = cx.background_executor().block(tool_result.output);
|
||||
|
||||
// Attach a project_notification tool call to the latest existing
|
||||
// Assistant message. We cannot create a new Assistant message
|
||||
// because thinking models require a `thinking` block that we
|
||||
// cannot mock. We cannot send a notification as a normal
|
||||
// (non-tool-use) User message because this distracts Agent
|
||||
// too much.
|
||||
let tool_message_id = self
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rfind(|(_, message)| message.role == Role::Assistant)
|
||||
.map(|(_, message)| message.id)?;
|
||||
|
||||
let tool_use_metadata = ToolUseMetadata {
|
||||
model: model.clone(),
|
||||
thread_id: self.id.clone(),
|
||||
prompt_id: self.last_prompt_id.clone(),
|
||||
};
|
||||
|
||||
self.tool_use
|
||||
.request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
|
||||
|
||||
let pending_tool_use = self.tool_use.insert_tool_output(
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
tool_output,
|
||||
self.configured_model.as_ref(),
|
||||
);
|
||||
|
||||
pending_tool_use
|
||||
}
|
||||
|
||||
pub fn stream_completion(
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
|
@ -1531,82 +1637,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
let event = match event {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
match error {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
anyhow::bail!(LanguageModelKnownError::RateLimitExceeded { retry_after });
|
||||
}
|
||||
LanguageModelCompletionError::Overloaded => {
|
||||
anyhow::bail!(LanguageModelKnownError::Overloaded);
|
||||
}
|
||||
LanguageModelCompletionError::ApiInternalServerError =>{
|
||||
anyhow::bail!(LanguageModelKnownError::ApiInternalServerError);
|
||||
}
|
||||
LanguageModelCompletionError::PromptTooLarge { tokens } => {
|
||||
let tokens = tokens.unwrap_or_else(|| {
|
||||
// We didn't get an exact token count from the API, so fall back on our estimate.
|
||||
thread.total_token_usage()
|
||||
.map(|usage| usage.total)
|
||||
.unwrap_or(0)
|
||||
// We know the context window was exceeded in practice, so if our estimate was
|
||||
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
|
||||
.max(model.max_token_count().saturating_add(1))
|
||||
});
|
||||
|
||||
anyhow::bail!(LanguageModelKnownError::ContextWindowLimitExceeded { tokens })
|
||||
}
|
||||
LanguageModelCompletionError::ApiReadResponseError(io_error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::ReadResponseError(io_error));
|
||||
}
|
||||
LanguageModelCompletionError::UnknownResponseFormat(error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::UnknownResponseFormat(error));
|
||||
}
|
||||
LanguageModelCompletionError::HttpResponseError { status, ref body } => {
|
||||
if let Some(known_error) = LanguageModelKnownError::from_http_response(status, body) {
|
||||
anyhow::bail!(known_error);
|
||||
} else {
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionError::DeserializeResponse(error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::DeserializeResponse(error));
|
||||
}
|
||||
LanguageModelCompletionError::BadInputJson {
|
||||
id,
|
||||
tool_name,
|
||||
raw_input: invalid_input_json,
|
||||
json_parse_error,
|
||||
} => {
|
||||
thread.receive_invalid_tool_json(
|
||||
id,
|
||||
tool_name,
|
||||
invalid_input_json,
|
||||
json_parse_error,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
// These are all errors we can't automatically attempt to recover from (e.g. by retrying)
|
||||
err @ LanguageModelCompletionError::BadRequestFormat |
|
||||
err @ LanguageModelCompletionError::AuthenticationError |
|
||||
err @ LanguageModelCompletionError::PermissionError |
|
||||
err @ LanguageModelCompletionError::ApiEndpointNotFound |
|
||||
err @ LanguageModelCompletionError::SerializeRequest(_) |
|
||||
err @ LanguageModelCompletionError::BuildRequestBody(_) |
|
||||
err @ LanguageModelCompletionError::HttpSend(_) => {
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
LanguageModelCompletionError::Other(error) => {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
match event? {
|
||||
LanguageModelCompletionEvent::StartMessage { .. } => {
|
||||
request_assistant_message_id =
|
||||
Some(thread.insert_assistant_message(
|
||||
|
@ -1683,9 +1714,7 @@ impl Thread {
|
|||
};
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::RedactedThinking {
|
||||
data
|
||||
} => {
|
||||
LanguageModelCompletionEvent::RedactedThinking { data } => {
|
||||
thread.received_chunk();
|
||||
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
|
@ -1734,6 +1763,21 @@ impl Thread {
|
|||
});
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id,
|
||||
tool_name,
|
||||
raw_input: invalid_input_json,
|
||||
json_parse_error,
|
||||
} => {
|
||||
thread.receive_invalid_tool_json(
|
||||
id,
|
||||
tool_name,
|
||||
invalid_input_json,
|
||||
json_parse_error,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
|
||||
if let Some(completion) = thread
|
||||
.pending_completions
|
||||
|
@ -1741,23 +1785,34 @@ impl Thread {
|
|||
.find(|completion| completion.id == pending_completion_id)
|
||||
{
|
||||
match status_update {
|
||||
CompletionRequestStatus::Queued {
|
||||
position,
|
||||
} => {
|
||||
completion.queue_state = QueueState::Queued { position };
|
||||
CompletionRequestStatus::Queued { position } => {
|
||||
completion.queue_state =
|
||||
QueueState::Queued { position };
|
||||
}
|
||||
CompletionRequestStatus::Started => {
|
||||
completion.queue_state = QueueState::Started;
|
||||
completion.queue_state = QueueState::Started;
|
||||
}
|
||||
CompletionRequestStatus::Failed {
|
||||
code, message, request_id
|
||||
code,
|
||||
message,
|
||||
request_id: _,
|
||||
retry_after,
|
||||
} => {
|
||||
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
|
||||
return Err(
|
||||
LanguageModelCompletionError::from_cloud_failure(
|
||||
model.upstream_provider_name(),
|
||||
code,
|
||||
message,
|
||||
retry_after.map(Duration::from_secs_f64),
|
||||
),
|
||||
);
|
||||
}
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount, limit
|
||||
} => {
|
||||
thread.update_model_request_usage(amount as u32, limit, cx);
|
||||
CompletionRequestStatus::UsageUpdated { amount, limit } => {
|
||||
thread.update_model_request_usage(
|
||||
amount as u32,
|
||||
limit,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
CompletionRequestStatus::ToolUseLimitReached => {
|
||||
thread.tool_use_limit_reached = true;
|
||||
|
@ -1808,10 +1863,11 @@ impl Thread {
|
|||
Ok(stop_reason) => {
|
||||
match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
let tool_uses = thread.use_pending_tools(window, model.clone(), cx);
|
||||
let tool_uses =
|
||||
thread.use_pending_tools(window, model.clone(), cx);
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn | StopReason::MaxTokens => {
|
||||
StopReason::EndTurn | StopReason::MaxTokens => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
|
@ -1827,7 +1883,9 @@ impl Thread {
|
|||
{
|
||||
let mut messages_to_remove = Vec::new();
|
||||
|
||||
for (ix, message) in thread.messages.iter().enumerate().rev() {
|
||||
for (ix, message) in
|
||||
thread.messages.iter().enumerate().rev()
|
||||
{
|
||||
messages_to_remove.push(message.id);
|
||||
|
||||
if message.role == Role::User {
|
||||
|
@ -1835,7 +1893,9 @@ impl Thread {
|
|||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_message) = thread.messages.get(ix - 1) {
|
||||
if let Some(prev_message) =
|
||||
thread.messages.get(ix - 1)
|
||||
{
|
||||
if prev_message.role == Role::Assistant {
|
||||
break;
|
||||
}
|
||||
|
@ -1850,14 +1910,16 @@ impl Thread {
|
|||
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Language model refusal".into(),
|
||||
message: "Model refused to generate content for safety reasons.".into(),
|
||||
message:
|
||||
"Model refused to generate content for safety reasons."
|
||||
.into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// We successfully completed, so cancel any remaining retries.
|
||||
thread.retry_state = None;
|
||||
},
|
||||
}
|
||||
Err(error) => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
|
@ -1883,26 +1945,38 @@ impl Thread {
|
|||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::ModelRequestLimitReached { plan: error.plan },
|
||||
));
|
||||
} else if let Some(known_error) =
|
||||
error.downcast_ref::<LanguageModelKnownError>()
|
||||
} else if let Some(completion_error) =
|
||||
error.downcast_ref::<LanguageModelCompletionError>()
|
||||
{
|
||||
match known_error {
|
||||
LanguageModelKnownError::ContextWindowLimitExceeded { tokens } => {
|
||||
use LanguageModelCompletionError::*;
|
||||
match &completion_error {
|
||||
PromptTooLarge { tokens, .. } => {
|
||||
let tokens = tokens.unwrap_or_else(|| {
|
||||
// We didn't get an exact token count from the API, so fall back on our estimate.
|
||||
thread
|
||||
.total_token_usage()
|
||||
.map(|usage| usage.total)
|
||||
.unwrap_or(0)
|
||||
// We know the context window was exceeded in practice, so if our estimate was
|
||||
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
|
||||
.max(model.max_token_count().saturating_add(1))
|
||||
});
|
||||
thread.exceeded_window_error = Some(ExceededWindowError {
|
||||
model_id: model.id(),
|
||||
token_count: *tokens,
|
||||
token_count: tokens,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
LanguageModelKnownError::RateLimitExceeded { retry_after } => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API rate limit exceeded",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
RateLimitExceeded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
}
|
||||
| ServerOverloaded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
} => {
|
||||
thread.handle_rate_limit_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
*retry_after,
|
||||
model.clone(),
|
||||
intent,
|
||||
|
@ -1911,15 +1985,9 @@ impl Thread {
|
|||
);
|
||||
retry_scheduled = true;
|
||||
}
|
||||
LanguageModelKnownError::Overloaded => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API servers are overloaded right now",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
RateLimitExceeded { .. } | ServerOverloaded { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
|
@ -1929,15 +1997,11 @@ impl Thread {
|
|||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
LanguageModelKnownError::ApiInternalServerError => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API server reported an internal server error",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
ApiInternalServerError { .. }
|
||||
| ApiReadResponseError { .. }
|
||||
| HttpSend { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
|
@ -1947,12 +2011,16 @@ impl Thread {
|
|||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
LanguageModelKnownError::ReadResponseError(_) |
|
||||
LanguageModelKnownError::DeserializeResponse(_) |
|
||||
LanguageModelKnownError::UnknownResponseFormat(_) => {
|
||||
// In the future we will attempt to re-roll response, but only once
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
NoApiKey { .. }
|
||||
| HttpResponseError { .. }
|
||||
| BadRequestFormat { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| Other { .. } => emit_generic_error(error, cx),
|
||||
}
|
||||
} else {
|
||||
emit_generic_error(error, cx);
|
||||
|
@ -2084,7 +2152,7 @@ impl Thread {
|
|||
|
||||
fn handle_rate_limit_error(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
retry_after: Duration,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
|
@ -2092,9 +2160,10 @@ impl Thread {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// For rate limit errors, we only retry once with the specified duration
|
||||
let retry_message = format!(
|
||||
"{error_message}. Retrying in {} seconds…",
|
||||
retry_after.as_secs()
|
||||
let retry_message = format!("{error}. Retrying in {} seconds…", retry_after.as_secs());
|
||||
log::warn!(
|
||||
"Retrying completion request in {} seconds: {error:?}",
|
||||
retry_after.as_secs(),
|
||||
);
|
||||
|
||||
// Add a UI-only message instead of a regular message
|
||||
|
@ -2127,18 +2196,18 @@ impl Thread {
|
|||
|
||||
fn handle_retryable_error(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.handle_retryable_error_with_delay(error_message, None, model, intent, window, cx)
|
||||
self.handle_retryable_error_with_delay(error, None, model, intent, window, cx)
|
||||
}
|
||||
|
||||
fn handle_retryable_error_with_delay(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
custom_delay: Option<Duration>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
|
@ -2168,8 +2237,12 @@ impl Thread {
|
|||
// Add a transient message to inform the user
|
||||
let delay_secs = delay.as_secs();
|
||||
let retry_message = format!(
|
||||
"{}. Retrying (attempt {} of {}) in {} seconds...",
|
||||
error_message, attempt, max_attempts, delay_secs
|
||||
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds..."
|
||||
);
|
||||
log::warn!(
|
||||
"Retrying completion request (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds: {error:?}",
|
||||
);
|
||||
|
||||
// Add a UI-only message instead of a regular message
|
||||
|
@ -2420,7 +2493,7 @@ impl Thread {
|
|||
|
||||
let tool_list = available_tools
|
||||
.iter()
|
||||
.map(|tool| format!("- {}: {}", tool.name(), tool.description()))
|
||||
.map(|(name, tool)| format!("- {}: {}", name, tool.description()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
|
@ -2640,7 +2713,7 @@ impl Thread {
|
|||
.profile
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.map(|tool| tool.name())
|
||||
.map(|(name, _)| name.clone().into())
|
||||
.collect();
|
||||
|
||||
self.message_feedback.insert(message_id, feedback);
|
||||
|
@ -3178,85 +3251,6 @@ struct PendingCompletion {
|
|||
_task: Task<()>,
|
||||
}
|
||||
|
||||
/// Resolves tool name conflicts by ensuring all tool names are unique.
|
||||
///
|
||||
/// When multiple tools have the same name, this function applies the following rules:
|
||||
/// 1. Native tools always keep their original name
|
||||
/// 2. Context server tools get prefixed with their server ID and an underscore
|
||||
/// 3. All tool names are truncated to MAX_TOOL_NAME_LENGTH (64 characters)
|
||||
/// 4. If conflicts still exist after prefixing, the conflicting tools are filtered out
|
||||
///
|
||||
/// Note: This function assumes that built-in tools occur before MCP tools in the tools list.
|
||||
fn resolve_tool_name_conflicts(tools: &[Arc<dyn Tool>]) -> Vec<(String, Arc<dyn Tool>)> {
|
||||
fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
|
||||
let mut tool_name = tool.name();
|
||||
tool_name.truncate(MAX_TOOL_NAME_LENGTH);
|
||||
tool_name
|
||||
}
|
||||
|
||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
|
||||
let mut duplicated_tool_names = HashSet::default();
|
||||
let mut seen_tool_names = HashSet::default();
|
||||
for tool in tools {
|
||||
let tool_name = resolve_tool_name(tool);
|
||||
if seen_tool_names.contains(&tool_name) {
|
||||
debug_assert!(
|
||||
tool.source() != assistant_tool::ToolSource::Native,
|
||||
"There are two built-in tools with the same name: {}",
|
||||
tool_name
|
||||
);
|
||||
duplicated_tool_names.insert(tool_name);
|
||||
} else {
|
||||
seen_tool_names.insert(tool_name);
|
||||
}
|
||||
}
|
||||
|
||||
if duplicated_tool_names.is_empty() {
|
||||
return tools
|
||||
.into_iter()
|
||||
.map(|tool| (resolve_tool_name(tool), tool.clone()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
tools
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let mut tool_name = resolve_tool_name(tool);
|
||||
if !duplicated_tool_names.contains(&tool_name) {
|
||||
return Some((tool_name, tool.clone()));
|
||||
}
|
||||
match tool.source() {
|
||||
assistant_tool::ToolSource::Native => {
|
||||
// Built-in tools always keep their original name
|
||||
Some((tool_name, tool.clone()))
|
||||
}
|
||||
assistant_tool::ToolSource::ContextServer { id } => {
|
||||
// Context server tools are prefixed with the context server ID, and truncated if necessary
|
||||
tool_name.insert(0, '_');
|
||||
if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
|
||||
let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
|
||||
let mut id = id.to_string();
|
||||
id.truncate(len);
|
||||
tool_name.insert_str(0, &id);
|
||||
} else {
|
||||
tool_name.insert_str(0, &id);
|
||||
}
|
||||
|
||||
tool_name.truncate(MAX_TOOL_NAME_LENGTH);
|
||||
|
||||
if seen_tool_names.contains(&tool_name) {
|
||||
log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
|
||||
None
|
||||
} else {
|
||||
Some((tool_name, tool.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -3268,11 +3262,13 @@ mod tests {
|
|||
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use assistant_tools;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::BoxStream;
|
||||
use gpui::TestAppContext;
|
||||
use icons::IconName;
|
||||
use http_client;
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
|
||||
|
@ -3600,6 +3596,105 @@ fn main() {{
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Add a buffer to the context. This will be a tracked buffer
|
||||
let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context = context_store
|
||||
.read_with(cx, |store, _| store.context().next().cloned())
|
||||
.unwrap();
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
|
||||
// Insert user message and assistant response
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx);
|
||||
thread.insert_assistant_message(
|
||||
vec![MessageSegment::Text("This code prints 42.".into())],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// We shouldn't have a stale buffer notification yet
|
||||
let notification = thread.read_with(cx, |thread, _| {
|
||||
find_tool_use(thread, "project_notifications")
|
||||
});
|
||||
assert!(
|
||||
notification.is_none(),
|
||||
"Should not have stale buffer notification before buffer is modified"
|
||||
);
|
||||
|
||||
// Modify the buffer
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(1..1, "\n println!(\"Added a new line\");\n")],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Insert another user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"What does the code do now?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Check for the stale buffer warning
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
let Some(notification_result) = thread.read_with(cx, |thread, _cx| {
|
||||
find_tool_use(thread, "project_notifications")
|
||||
}) else {
|
||||
panic!("Should have a `project_notifications` tool use");
|
||||
};
|
||||
|
||||
let Some(notification_content) = notification_result.content.to_str() else {
|
||||
panic!("`project_notifications` should return text");
|
||||
};
|
||||
|
||||
let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
- code.rs
|
||||
"};
|
||||
assert_eq!(notification_content, expected_content);
|
||||
}
|
||||
|
||||
fn find_tool_use(thread: &Thread, tool_name: &str) -> Option<LanguageModelToolResult> {
|
||||
thread
|
||||
.messages()
|
||||
.filter_map(|message| {
|
||||
thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.find(|result| result.tool_name == tool_name.into())
|
||||
})
|
||||
.next()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
@ -3917,148 +4012,6 @@ fn main() {{
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_tool_name_conflicts() {
|
||||
use assistant_tool::{Tool, ToolSource};
|
||||
|
||||
assert_resolve_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
|
||||
],
|
||||
vec!["tool1", "tool2", "tool3"],
|
||||
);
|
||||
|
||||
assert_resolve_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
|
||||
],
|
||||
vec!["tool1", "tool2", "mcp-1_tool3", "mcp-2_tool3"],
|
||||
);
|
||||
|
||||
assert_resolve_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
TestTool::new("tool3", ToolSource::Native),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
|
||||
],
|
||||
vec!["tool1", "tool2", "tool3", "mcp-1_tool3", "mcp-2_tool3"],
|
||||
);
|
||||
|
||||
// Test that tool with very long name is always truncated
|
||||
assert_resolve_tool_name_conflicts(
|
||||
vec![TestTool::new(
|
||||
"tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-blah-blah",
|
||||
ToolSource::Native,
|
||||
)],
|
||||
vec!["tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-"],
|
||||
);
|
||||
|
||||
// Test deduplication of tools with very long names, in this case the mcp server name should be truncated
|
||||
assert_resolve_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool-with-very-very-very-long-name", ToolSource::Native),
|
||||
TestTool::new(
|
||||
"tool-with-very-very-very-long-name",
|
||||
ToolSource::ContextServer {
|
||||
id: "mcp-with-very-very-very-long-name".into(),
|
||||
},
|
||||
),
|
||||
],
|
||||
vec![
|
||||
"tool-with-very-very-very-long-name",
|
||||
"mcp-with-very-very-very-long-_tool-with-very-very-very-long-name",
|
||||
],
|
||||
);
|
||||
|
||||
fn assert_resolve_tool_name_conflicts(
|
||||
tools: Vec<TestTool>,
|
||||
expected: Vec<impl Into<String>>,
|
||||
) {
|
||||
let tools: Vec<Arc<dyn Tool>> = tools
|
||||
.into_iter()
|
||||
.map(|t| Arc::new(t) as Arc<dyn Tool>)
|
||||
.collect();
|
||||
let tools = resolve_tool_name_conflicts(&tools);
|
||||
assert_eq!(tools.len(), expected.len());
|
||||
for (i, expected_name) in expected.into_iter().enumerate() {
|
||||
let expected_name = expected_name.into();
|
||||
let actual_name = &tools[i].0;
|
||||
assert_eq!(
|
||||
actual_name, &expected_name,
|
||||
"Expected '{}' got '{}' at index {}",
|
||||
expected_name, actual_name, i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct TestTool {
|
||||
name: String,
|
||||
source: ToolSource,
|
||||
}
|
||||
|
||||
impl TestTool {
|
||||
fn new(name: impl Into<String>, source: ToolSource) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for TestTool {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Ai
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn source(&self) -> ToolSource {
|
||||
self.source.clone()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Test tool".to_string()
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Test tool".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> assistant_tool::ToolResult {
|
||||
assistant_tool::ToolResult {
|
||||
output: Task::ready(Err(anyhow::anyhow!("No content"))),
|
||||
card: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a model that returns errors
|
||||
enum TestError {
|
||||
Overloaded,
|
||||
|
@ -4139,9 +4092,15 @@ fn main() {{
|
|||
>,
|
||||
> {
|
||||
let error = match self.error_type {
|
||||
TestError::Overloaded => LanguageModelCompletionError::Overloaded,
|
||||
TestError::Overloaded => LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: self.provider_name(),
|
||||
retry_after: None,
|
||||
},
|
||||
TestError::InternalServerError => {
|
||||
LanguageModelCompletionError::ApiInternalServerError
|
||||
LanguageModelCompletionError::ApiInternalServerError {
|
||||
provider: self.provider_name(),
|
||||
message: "I'm a teapot orbiting the sun".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
async move {
|
||||
|
@ -4649,9 +4608,13 @@ fn main() {{
|
|||
> {
|
||||
if !*self.failed_once.lock() {
|
||||
*self.failed_once.lock() = true;
|
||||
let provider = self.provider_name();
|
||||
// Return error on first attempt
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::Overloaded)
|
||||
Err(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
})
|
||||
});
|
||||
async move { Ok(stream.boxed()) }.boxed()
|
||||
} else {
|
||||
|
@ -4814,9 +4777,13 @@ fn main() {{
|
|||
> {
|
||||
if !*self.failed_once.lock() {
|
||||
*self.failed_once.lock() = true;
|
||||
let provider = self.provider_name();
|
||||
// Return error on first attempt
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::Overloaded)
|
||||
Err(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
})
|
||||
});
|
||||
async move { Ok(stream.boxed()) }.boxed()
|
||||
} else {
|
||||
|
@ -4969,10 +4936,12 @@ fn main() {{
|
|||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let provider = self.provider_name();
|
||||
async move {
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::RateLimitExceeded {
|
||||
retry_after: Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS),
|
||||
provider,
|
||||
retry_after: Some(Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS)),
|
||||
})
|
||||
});
|
||||
Ok(stream.boxed())
|
||||
|
@ -5291,6 +5260,14 @@ fn main() {{
|
|||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
ToolRegistry::default_global(cx);
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let http_client = Arc::new(http_client::HttpClientWithUrl::new(
|
||||
http_client::FakeHttpClient::with_200_response(),
|
||||
"http://localhost".to_string(),
|
||||
None,
|
||||
));
|
||||
assistant_tools::init(http_client, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
};
|
||||
use agent_settings::{AgentProfileId, CompletionMode};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use assistant_tool::{Tool, ToolId, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
|
@ -537,8 +537,8 @@ impl ThreadStore {
|
|||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||
tool_working_set.update(cx, |tool_working_set, _| {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
tool_working_set.update(cx, |tool_working_set, cx| {
|
||||
tool_working_set.remove(&tool_ids, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -569,19 +569,17 @@ impl ThreadStore {
|
|||
.log_err()
|
||||
{
|
||||
let tool_ids = tool_working_set
|
||||
.update(cx, |tool_working_set, _| {
|
||||
response
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
log::info!("registering context server tool: {:?}", tool.name);
|
||||
tool_working_set.insert(Arc::new(ContextServerTool::new(
|
||||
.update(cx, |tool_working_set, cx| {
|
||||
tool_working_set.extend(
|
||||
response.tools.into_iter().map(|tool| {
|
||||
Arc::new(ContextServerTool::new(
|
||||
context_server_store.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
)) as Arc<dyn Tool>
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@ use anyhow::{Result, bail};
|
|||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
|
@ -49,7 +50,7 @@ pub struct AgentSettings {
|
|||
pub dock: AgentDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
|
@ -211,7 +212,6 @@ impl AgentSettingsContent {
|
|||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
|
@ -321,29 +321,27 @@ pub struct LanguageModelSelection {
|
|||
pub struct LanguageModelProviderSetting(pub String);
|
||||
|
||||
impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"LanguageModelProviderSetting".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"amazon-bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
"openrouter".into(),
|
||||
"mistral".into(),
|
||||
"vercel".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"anthropic",
|
||||
"amazon-bedrock",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"ollama",
|
||||
"openai",
|
||||
"zed.dev",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"openrouter",
|
||||
"mistral",
|
||||
"vercel"
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,15 +357,6 @@ impl From<&str> for LanguageModelProviderSetting {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: LanguageModelProviderSetting("openai".to_string()),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
|
@ -411,7 +400,10 @@ impl Settings for AgentSettings {
|
|||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model.clone());
|
||||
settings.default_model = value
|
||||
.default_model
|
||||
.clone()
|
||||
.or(settings.default_model.take());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use crate::context_picker::{ContextPicker, MentionLink};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::message_editor::{extract_message_creases, insert_message_creases};
|
||||
use crate::ui::{
|
||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||
};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AgentPanel, ModelUsageContext};
|
||||
use agent::{
|
||||
ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore,
|
||||
|
@ -1026,6 +1024,7 @@ impl ActiveThread {
|
|||
}
|
||||
}
|
||||
ThreadEvent::MessageAdded(message_id) => {
|
||||
self.clear_last_error();
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
RenderedMessage::from_segments(
|
||||
|
@ -1042,6 +1041,7 @@ impl ActiveThread {
|
|||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageEdited(message_id) => {
|
||||
self.clear_last_error();
|
||||
if let Some(index) = self.messages.iter().position(|id| id == message_id) {
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
|
@ -1818,7 +1818,7 @@ impl ActiveThread {
|
|||
.my_3()
|
||||
.mx_5()
|
||||
.when(is_generating_stale || message.is_hidden, |this| {
|
||||
this.child(AnimatedLabel::new("").size(LabelSize::Small))
|
||||
this.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -2584,7 +2584,7 @@ impl ActiveThread {
|
|||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(AnimatedLabel::new("Thinking").size(LabelSize::Small)),
|
||||
.child(LoadingLabel::new("Thinking").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
@ -3153,7 +3153,7 @@ impl ActiveThread {
|
|||
.border_color(self.tool_card_border_color(cx))
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
|
@ -16,7 +16,9 @@ use gpui::{
|
|||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
|
@ -24,8 +26,8 @@ use project::{
|
|||
};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState,
|
||||
Switch, SwitchColor, Tooltip, prelude::*,
|
||||
ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
@ -86,6 +88,14 @@ impl AgentConfiguration {
|
|||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut expanded_provider_configurations = HashMap::default();
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.provider(&ZED_CLOUD_PROVIDER_ID)
|
||||
.map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
|
||||
{
|
||||
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
|
@ -94,7 +104,7 @@ impl AgentConfiguration {
|
|||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
expanded_provider_configurations,
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
|
@ -162,19 +172,29 @@ impl AgentConfiguration {
|
|||
.unwrap_or(false);
|
||||
|
||||
v_flex()
|
||||
.py_2()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.when(is_expanded, |this| this.mb_2())
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.6)
|
||||
.px_2()
|
||||
.child(Divider::horizontal().color(DividerColor::Border)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.map(|this| {
|
||||
if is_expanded {
|
||||
this.mt_2().mb_1()
|
||||
} else {
|
||||
this.my_2()
|
||||
}
|
||||
})
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(provider_id_string.clone())
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
|
@ -237,12 +257,16 @@ impl AgentConfiguration {
|
|||
)
|
||||
}),
|
||||
)
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
|
@ -252,12 +276,11 @@ impl AgentConfiguration {
|
|||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.pb_0()
|
||||
.mb_2p5()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
|
@ -266,10 +289,15 @@ impl AgentConfiguration {
|
|||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_configuration_block(&provider, cx)),
|
||||
.child(
|
||||
div()
|
||||
.pl(DynamicSpacing::Base08.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.children(
|
||||
providers.into_iter().map(|provider| {
|
||||
self.render_provider_configuration_block(&provider, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -408,7 +436,7 @@ impl AgentConfiguration {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
|
||||
let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
|
|
|
@ -379,6 +379,14 @@ impl ConfigureContextServerModal {
|
|||
};
|
||||
|
||||
self.state = State::Waiting;
|
||||
|
||||
let existing_server = self.context_server_store.read(cx).get_running_server(&id);
|
||||
if existing_server.is_some() {
|
||||
self.context_server_store.update(cx, |store, cx| {
|
||||
store.stop_server(&id, cx).log_err();
|
||||
});
|
||||
}
|
||||
|
||||
let wait_for_context_server_task =
|
||||
wait_for_context_server(&self.context_server_store, id.clone(), cx);
|
||||
cx.spawn({
|
||||
|
@ -399,13 +407,21 @@ impl ConfigureContextServerModal {
|
|||
})
|
||||
.detach();
|
||||
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
let settings_changed =
|
||||
ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
|
||||
|
||||
if settings_changed {
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if let Some(existing_server) = existing_server {
|
||||
self.context_server_store
|
||||
.update(cx, |store, cx| store.start_server(existing_server, cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
|
|
|
@ -41,7 +41,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
|
||||
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
||||
linear_gradient, prelude::*, pulsating_between,
|
||||
};
|
||||
|
@ -59,7 +59,7 @@ use theme::ThemeSettings;
|
|||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
@ -2025,9 +2025,7 @@ impl AgentPanel {
|
|||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
});
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
if !is_using_zed_provider {
|
||||
return false;
|
||||
|
@ -2600,7 +2598,7 @@ impl AgentPanel {
|
|||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.child(Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadtEmptyState,
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
|
@ -2691,58 +2689,90 @@ impl AgentPanel {
|
|||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_error_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn upgrade_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme().status().error.opacity(0.08)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
const ERROR_MESSAGE: &str =
|
||||
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(ERROR_MESSAGE))
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Free Usage Exceeded")
|
||||
.description(ERROR_MESSAGE)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(ERROR_MESSAGE))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
|
@ -2752,67 +2782,28 @@ impl AgentPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let error_message = match plan {
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
Plan::ZedProTrial => {
|
||||
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
||||
}
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
};
|
||||
let call_to_action = match plan {
|
||||
Plan::ZedPro => "Upgrade to usage-based billing",
|
||||
Plan::ZedProTrial => "Upgrade to Zed Pro",
|
||||
Plan::Free => "Upgrade to Zed Pro",
|
||||
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(error_message))
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Model Prompt Limit Reached")
|
||||
.description(error_message)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(error_message))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
|
@ -2823,40 +2814,24 @@ impl AgentPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let message_with_header = format!("{}\n{}", header, message);
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(header).weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_32()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(message_with_header))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title(header)
|
||||
.description(message.clone())
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(message_with_header))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_prompt_editor(
|
||||
|
@ -3001,15 +2976,6 @@ impl AgentPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
|
@ -3091,18 +3057,9 @@ impl Render for AgentPanel {
|
|||
thread.clone().into_any_element()
|
||||
})
|
||||
.children(self.render_tool_use_limit_reached(window, cx))
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => {
|
||||
self.render_payment_required_error(thread, cx)
|
||||
|
@ -3116,6 +3073,7 @@ impl Render for AgentPanel {
|
|||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::TextThread {
|
||||
|
|
|
@ -54,51 +54,88 @@ pub use ui::preview::{all_agent_previews, get_agent_preview};
|
|||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||
ToggleContextPicker,
|
||||
/// Toggles the navigation menu for switching between threads and views.
|
||||
ToggleNavigationMenu,
|
||||
/// Toggles the options menu for agent settings and preferences.
|
||||
ToggleOptionsMenu,
|
||||
/// Deletes the recently opened thread from history.
|
||||
DeleteRecentlyOpenThread,
|
||||
/// Toggles the profile selector for switching between agent profiles.
|
||||
ToggleProfileSelector,
|
||||
/// Removes all added context from the current conversation.
|
||||
RemoveAllContext,
|
||||
/// Expands the message editor to full size.
|
||||
ExpandMessageEditor,
|
||||
/// Opens the conversation history view.
|
||||
OpenHistory,
|
||||
/// Adds a context server to the configuration.
|
||||
AddContextServer,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
/// Starts a chat conversation with the agent.
|
||||
Chat,
|
||||
/// Starts a chat conversation with follow-up enabled.
|
||||
ChatWithFollow,
|
||||
/// Cycles to the next inline assist suggestion.
|
||||
CycleNextInlineAssist,
|
||||
/// Cycles to the previous inline assist suggestion.
|
||||
CyclePreviousInlineAssist,
|
||||
/// Moves focus up in the interface.
|
||||
FocusUp,
|
||||
/// Moves focus down in the interface.
|
||||
FocusDown,
|
||||
/// Moves focus left in the interface.
|
||||
FocusLeft,
|
||||
/// Moves focus right in the interface.
|
||||
FocusRight,
|
||||
/// Removes the currently focused context item.
|
||||
RemoveFocusedContext,
|
||||
/// Accepts the suggested context item.
|
||||
AcceptSuggestedContext,
|
||||
/// Opens the active thread as a markdown file.
|
||||
OpenActiveThreadAsMarkdown,
|
||||
/// Opens the agent diff view to review changes.
|
||||
OpenAgentDiff,
|
||||
/// Keeps the current suggestion or change.
|
||||
Keep,
|
||||
/// Rejects the current suggestion or change.
|
||||
Reject,
|
||||
/// Rejects all suggestions or changes.
|
||||
RejectAll,
|
||||
/// Keeps all suggestions or changes.
|
||||
KeepAll,
|
||||
/// Follows the agent's suggestions.
|
||||
Follow,
|
||||
/// Resets the trial upsell notification.
|
||||
ResetTrialUpsell,
|
||||
/// Resets the trial end upsell notification.
|
||||
ResetTrialEndUpsell,
|
||||
/// Continues the current thread.
|
||||
ContinueThread,
|
||||
/// Continues the thread with burn mode enabled.
|
||||
ContinueWithBurnMode,
|
||||
/// Toggles burn mode for faster responses.
|
||||
ToggleBurnMode,
|
||||
]
|
||||
);
|
||||
|
||||
/// Creates a new conversation thread, optionally based on an existing thread.
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// Opens the profile management interface for configuring agent tools and settings.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
|
@ -209,7 +246,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
|||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let default = settings.default_model.as_ref().map(to_selected_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
|
@ -229,7 +266,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_default_model(default.as_ref(), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
|
|
|
@ -426,6 +426,7 @@ impl ContextPicker {
|
|||
this.add_recent_file(project_path.clone(), window, cx);
|
||||
})
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
RecentEntry::Thread(thread) => {
|
||||
|
@ -443,6 +444,7 @@ impl ContextPicker {
|
|||
.detach_and_log_err(cx);
|
||||
})
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider {
|
|||
let mut label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
|
||||
|
||||
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
|
||||
let new_text_len = new_text.len();
|
||||
|
|
|
@ -18,6 +18,7 @@ use ui::{ListItem, ListItemSpacing, prelude::*};
|
|||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Toggles the language model selector dropdown.
|
||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||
ToggleModelSelector
|
||||
]
|
||||
|
|
|
@ -1250,9 +1250,7 @@ impl MessageEditor {
|
|||
self.thread
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
|
||||
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
|
||||
|
|
|
@ -85,16 +85,24 @@ use assistant_context::{
|
|||
actions!(
|
||||
assistant,
|
||||
[
|
||||
/// Sends the current message to the assistant.
|
||||
Assist,
|
||||
/// Confirms and executes the entered slash command.
|
||||
ConfirmCommand,
|
||||
/// Copies code from the assistant's response to the clipboard.
|
||||
CopyCode,
|
||||
/// Cycles between user and assistant message roles.
|
||||
CycleMessageRole,
|
||||
/// Inserts the selected text into the active editor.
|
||||
InsertIntoEditor,
|
||||
/// Quotes the current selection in the assistant conversation.
|
||||
QuoteSelection,
|
||||
/// Splits the conversation at the current cursor position.
|
||||
Split,
|
||||
]
|
||||
);
|
||||
|
||||
/// Inserts files that were dragged and dropped into the assistant conversation.
|
||||
#[derive(PartialEq, Clone, Action)]
|
||||
#[action(namespace = assistant, no_json, no_register)]
|
||||
pub enum InsertDraggedFiles {
|
||||
|
|
|
@ -42,8 +42,8 @@ impl IncompatibleToolsState {
|
|||
.profile()
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
|
||||
.cloned()
|
||||
.filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err())
|
||||
.map(|(_, tool)| tool.clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
mod agent_notification;
|
||||
mod animated_label;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod onboarding_modal;
|
||||
|
@ -7,7 +6,6 @@ pub mod preview;
|
|||
mod upsell;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
|
|
@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use chrono::{DateTime, Utc};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use http_client::http::{self, HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
|
@ -356,7 +356,7 @@ pub async fn complete(
|
|||
.send(request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
let status = response.status();
|
||||
let status_code = response.status();
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
|
@ -364,12 +364,12 @@ pub async fn complete(
|
|||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
if status.is_success() {
|
||||
if status_code.is_success() {
|
||||
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
|
||||
} else {
|
||||
Err(AnthropicError::HttpResponseError {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
status_code,
|
||||
message: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -444,11 +444,7 @@ impl RateLimitInfo {
|
|||
}
|
||||
|
||||
Self {
|
||||
retry_after: headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs),
|
||||
retry_after: parse_retry_after(headers),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
|
@ -457,6 +453,17 @@ impl RateLimitInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
|
||||
/// seconds). Note that other services might specify an HTTP date or some other format for this
|
||||
/// header. Returns `None` if the header is not present or cannot be parsed.
|
||||
pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
|
||||
headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
}
|
||||
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
|
||||
Ok(headers
|
||||
.get(key)
|
||||
|
@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if response.status().as_u16() == 529 {
|
||||
Err(AnthropicError::ServerOverloaded {
|
||||
retry_after: rate_limits.retry_after,
|
||||
})
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit { retry_after })
|
||||
} else {
|
||||
|
@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||
|
||||
match serde_json::from_str::<Event>(&body) {
|
||||
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
|
||||
Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
|
||||
Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status: response.status().as_u16(),
|
||||
body: body,
|
||||
Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status_code: response.status(),
|
||||
message: body,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -801,16 +811,19 @@ pub enum AnthropicError {
|
|||
ReadResponse(io::Error),
|
||||
|
||||
/// HTTP error response from the API
|
||||
HttpResponseError { status: u16, body: String },
|
||||
HttpResponseError {
|
||||
status_code: StatusCode,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Rate limit exceeded
|
||||
RateLimit { retry_after: Duration },
|
||||
|
||||
/// Server overloaded
|
||||
ServerOverloaded { retry_after: Option<Duration> },
|
||||
|
||||
/// API returned an error response
|
||||
ApiError(ApiError),
|
||||
|
||||
/// Unexpected response format
|
||||
UnexpectedResponseFormat(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Error)]
|
||||
|
|
|
@ -2140,7 +2140,8 @@ impl AssistantContext {
|
|||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(_) |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ gpui.workspace = true
|
|||
icons.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
|
|
|
@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> {
|
|||
// `additionalProperties` defaults to `false` unless explicitly specified.
|
||||
// This prevents models from hallucinating tool parameters.
|
||||
if let Value::Object(obj) = json {
|
||||
if let Some(Value::String(type_str)) = obj.get("type") {
|
||||
if type_str == "object" && !obj.contains_key("additionalProperties") {
|
||||
if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
|
||||
if !obj.contains_key("additionalProperties") {
|
||||
obj.insert("additionalProperties".to_string(), Value::Bool(false));
|
||||
}
|
||||
|
||||
// OpenAI API requires non-missing `properties`
|
||||
if !obj.contains_key("properties") {
|
||||
obj.insert("properties".to_string(), Value::Object(Default::default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -1,18 +1,52 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use collections::{HashMap, IndexMap};
|
||||
use gpui::App;
|
||||
use std::{borrow::Borrow, sync::Arc};
|
||||
|
||||
use crate::{Tool, ToolRegistry, ToolSource};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use gpui::{App, SharedString};
|
||||
use util::debug_panic;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct ToolId(usize);
|
||||
|
||||
/// A unique identifier for a tool within a working set.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct UniqueToolName(SharedString);
|
||||
|
||||
impl Borrow<str> for UniqueToolName {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for UniqueToolName {
|
||||
fn from(value: String) -> Self {
|
||||
UniqueToolName(SharedString::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for UniqueToolName {
|
||||
fn into(self) -> String {
|
||||
self.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UniqueToolName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UniqueToolName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// A working set of tools for use in one instance of the Assistant Panel.
|
||||
#[derive(Default)]
|
||||
pub struct ToolWorkingSet {
|
||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<UniqueToolName, Arc<dyn Tool>>,
|
||||
next_tool_id: ToolId,
|
||||
}
|
||||
|
||||
|
@ -24,16 +58,20 @@ impl ToolWorkingSet {
|
|||
.or_else(|| ToolRegistry::global(cx).tool(name))
|
||||
}
|
||||
|
||||
pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let mut tools = ToolRegistry::global(cx).tools();
|
||||
tools.extend(self.context_server_tools_by_id.values().cloned());
|
||||
pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
|
||||
let mut tools = ToolRegistry::global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.map(|tool| (UniqueToolName(tool.name().into()), tool))
|
||||
.collect::<Vec<_>>();
|
||||
tools.extend(self.context_server_tools_by_name.clone());
|
||||
tools
|
||||
}
|
||||
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
let mut tools_by_source = IndexMap::default();
|
||||
|
||||
for tool in self.tools(cx) {
|
||||
for (_, tool) in self.tools(cx) {
|
||||
tools_by_source
|
||||
.entry(tool.source())
|
||||
.or_insert_with(Vec::new)
|
||||
|
@ -49,27 +87,324 @@ impl ToolWorkingSet {
|
|||
tools_by_source
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
pub fn insert(&mut self, tool: Arc<dyn Tool>, cx: &App) -> ToolId {
|
||||
let tool_id = self.register_tool(tool);
|
||||
self.tools_changed(cx);
|
||||
tool_id
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, tools: impl Iterator<Item = Arc<dyn Tool>>, cx: &App) -> Vec<ToolId> {
|
||||
let ids = tools.map(|tool| self.register_tool(tool)).collect();
|
||||
self.tools_changed(cx);
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) {
|
||||
self.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
self.tools_changed(cx);
|
||||
}
|
||||
|
||||
fn register_tool(&mut self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let tool_id = self.next_tool_id;
|
||||
self.next_tool_id.0 += 1;
|
||||
self.context_server_tools_by_id
|
||||
.insert(tool_id, tool.clone());
|
||||
self.tools_changed();
|
||||
tool_id
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
|
||||
self.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
self.tools_changed();
|
||||
}
|
||||
|
||||
fn tools_changed(&mut self) {
|
||||
self.context_server_tools_by_name.clear();
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
fn tools_changed(&mut self, cx: &App) {
|
||||
self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts(
|
||||
&self
|
||||
.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
&ToolRegistry::global(cx).tools(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_context_server_tool_name_conflicts(
|
||||
context_server_tools: &[Arc<dyn Tool>],
|
||||
native_tools: &[Arc<dyn Tool>],
|
||||
) -> HashMap<UniqueToolName, Arc<dyn Tool>> {
|
||||
fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
|
||||
let mut tool_name = tool.name();
|
||||
tool_name.truncate(MAX_TOOL_NAME_LENGTH);
|
||||
tool_name
|
||||
}
|
||||
|
||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
|
||||
let mut duplicated_tool_names = HashSet::default();
|
||||
let mut seen_tool_names = HashSet::default();
|
||||
seen_tool_names.extend(native_tools.iter().map(|tool| tool.name()));
|
||||
for tool in context_server_tools {
|
||||
let tool_name = resolve_tool_name(tool);
|
||||
if seen_tool_names.contains(&tool_name) {
|
||||
debug_assert!(
|
||||
tool.source() != ToolSource::Native,
|
||||
"Expected MCP tool but got a native tool: {}",
|
||||
tool_name
|
||||
);
|
||||
duplicated_tool_names.insert(tool_name);
|
||||
} else {
|
||||
seen_tool_names.insert(tool_name);
|
||||
}
|
||||
}
|
||||
|
||||
if duplicated_tool_names.is_empty() {
|
||||
return context_server_tools
|
||||
.into_iter()
|
||||
.map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
context_server_tools
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let mut tool_name = resolve_tool_name(tool);
|
||||
if !duplicated_tool_names.contains(&tool_name) {
|
||||
return Some((tool_name.into(), tool.clone()));
|
||||
}
|
||||
match tool.source() {
|
||||
ToolSource::Native => {
|
||||
debug_panic!("Expected MCP tool but got a native tool: {}", tool_name);
|
||||
// Built-in tools always keep their original name
|
||||
Some((tool_name.into(), tool.clone()))
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
// Context server tools are prefixed with the context server ID, and truncated if necessary
|
||||
tool_name.insert(0, '_');
|
||||
if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
|
||||
let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
|
||||
let mut id = id.to_string();
|
||||
id.truncate(len);
|
||||
tool_name.insert_str(0, &id);
|
||||
} else {
|
||||
tool_name.insert_str(0, &id);
|
||||
}
|
||||
|
||||
tool_name.truncate(MAX_TOOL_NAME_LENGTH);
|
||||
|
||||
if seen_tool_names.contains(&tool_name) {
|
||||
log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
|
||||
None
|
||||
} else {
|
||||
Some((tool_name.into(), tool.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AnyWindowHandle, Entity, Task, TestAppContext};
|
||||
use language_model::{LanguageModel, LanguageModelRequest};
|
||||
use project::Project;
|
||||
|
||||
use crate::{ActionLog, ToolResult};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_unique_tool_names(cx: &mut TestAppContext) {
|
||||
fn assert_tool(
|
||||
tool_working_set: &ToolWorkingSet,
|
||||
unique_name: &str,
|
||||
expected_name: &str,
|
||||
expected_source: ToolSource,
|
||||
cx: &App,
|
||||
) {
|
||||
let tool = tool_working_set.tool(unique_name, cx).unwrap();
|
||||
assert_eq!(tool.name(), expected_name);
|
||||
assert_eq!(tool.source(), expected_source);
|
||||
}
|
||||
|
||||
let tool_registry = cx.update(ToolRegistry::default_global);
|
||||
tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native));
|
||||
tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native));
|
||||
|
||||
let mut tool_working_set = ToolWorkingSet::default();
|
||||
cx.update(|cx| {
|
||||
tool_working_set.extend(
|
||||
vec![
|
||||
Arc::new(TestTool::new(
|
||||
"tool2",
|
||||
ToolSource::ContextServer { id: "mcp-1".into() },
|
||||
)) as Arc<dyn Tool>,
|
||||
Arc::new(TestTool::new(
|
||||
"tool2",
|
||||
ToolSource::ContextServer { id: "mcp-2".into() },
|
||||
)) as Arc<dyn Tool>,
|
||||
]
|
||||
.into_iter(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx);
|
||||
assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx);
|
||||
assert_tool(
|
||||
&tool_working_set,
|
||||
"mcp-1_tool2",
|
||||
"tool2",
|
||||
ToolSource::ContextServer { id: "mcp-1".into() },
|
||||
cx,
|
||||
);
|
||||
assert_tool(
|
||||
&tool_working_set,
|
||||
"mcp-2_tool2",
|
||||
"tool2",
|
||||
ToolSource::ContextServer { id: "mcp-2".into() },
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_context_server_tool_name_conflicts() {
|
||||
assert_resolve_context_server_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
],
|
||||
vec![TestTool::new(
|
||||
"tool3",
|
||||
ToolSource::ContextServer { id: "mcp-1".into() },
|
||||
)],
|
||||
vec!["tool3"],
|
||||
);
|
||||
|
||||
assert_resolve_context_server_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
],
|
||||
vec![
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
|
||||
],
|
||||
vec!["mcp-1_tool3", "mcp-2_tool3"],
|
||||
);
|
||||
|
||||
assert_resolve_context_server_tool_name_conflicts(
|
||||
vec![
|
||||
TestTool::new("tool1", ToolSource::Native),
|
||||
TestTool::new("tool2", ToolSource::Native),
|
||||
TestTool::new("tool3", ToolSource::Native),
|
||||
],
|
||||
vec![
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
|
||||
TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
|
||||
],
|
||||
vec!["mcp-1_tool3", "mcp-2_tool3"],
|
||||
);
|
||||
|
||||
// Test deduplication of tools with very long names, in this case the mcp server name should be truncated
|
||||
assert_resolve_context_server_tool_name_conflicts(
|
||||
vec![TestTool::new(
|
||||
"tool-with-very-very-very-long-name",
|
||||
ToolSource::Native,
|
||||
)],
|
||||
vec![TestTool::new(
|
||||
"tool-with-very-very-very-long-name",
|
||||
ToolSource::ContextServer {
|
||||
id: "mcp-with-very-very-very-long-name".into(),
|
||||
},
|
||||
)],
|
||||
vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"],
|
||||
);
|
||||
|
||||
fn assert_resolve_context_server_tool_name_conflicts(
|
||||
builtin_tools: Vec<TestTool>,
|
||||
context_server_tools: Vec<TestTool>,
|
||||
expected: Vec<&'static str>,
|
||||
) {
|
||||
let context_server_tools: Vec<Arc<dyn Tool>> = context_server_tools
|
||||
.into_iter()
|
||||
.map(|t| Arc::new(t) as Arc<dyn Tool>)
|
||||
.collect();
|
||||
let builtin_tools: Vec<Arc<dyn Tool>> = builtin_tools
|
||||
.into_iter()
|
||||
.map(|t| Arc::new(t) as Arc<dyn Tool>)
|
||||
.collect();
|
||||
let tools =
|
||||
resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools);
|
||||
assert_eq!(tools.len(), expected.len());
|
||||
for (i, (name, _)) in tools.into_iter().enumerate() {
|
||||
assert_eq!(
|
||||
name.0.as_ref(),
|
||||
expected[i],
|
||||
"Expected '{}' got '{}' at index {}",
|
||||
expected[i],
|
||||
name,
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestTool {
|
||||
name: String,
|
||||
source: ToolSource,
|
||||
}
|
||||
|
||||
impl TestTool {
|
||||
fn new(name: impl Into<String>, source: ToolSource) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for TestTool {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> icons::IconName {
|
||||
icons::IconName::Ai
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn source(&self) -> ToolSource {
|
||||
self.source.clone()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Test tool".to_string()
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Test tool".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
ToolResult {
|
||||
output: Task::ready(Err(anyhow::anyhow!("No content"))),
|
||||
card: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ mod list_directory_tool;
|
|||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod project_notifications_tool;
|
||||
mod read_file_tool;
|
||||
mod schema;
|
||||
mod templates;
|
||||
|
@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput};
|
|||
pub use find_path_tool::FindPathToolInput;
|
||||
pub use grep_tool::{GrepTool, GrepToolInput};
|
||||
pub use open_tool::OpenTool;
|
||||
pub use project_notifications_tool::ProjectNotificationsTool;
|
||||
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
|
||||
pub use terminal_tool::TerminalTool;
|
||||
|
||||
|
@ -61,6 +63,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
|||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(ProjectNotificationsTool);
|
||||
registry.register_tool(FindPathTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(GrepTool);
|
||||
|
|
|
@ -29,6 +29,7 @@ use std::{
|
|||
path::Path,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::path;
|
||||
|
||||
|
@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
|||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
Ok(err) => match &err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
|
||||
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
|
||||
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
|
||||
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
);
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
|
|
|
@ -9132,7 +9132,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
|
||||
self.manipulate_lines(window, cx, |lines| lines.sort())
|
||||
}
|
||||
|
||||
pub fn sort_lines_case_insensitive(
|
||||
|
@ -9141,7 +9141,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
lines.sort_by_key(|line| line.to_lowercase())
|
||||
})
|
||||
}
|
||||
|
@ -9152,7 +9152,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(line.to_lowercase()));
|
||||
})
|
||||
|
@ -9164,7 +9164,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| {
|
||||
self.manipulate_lines(window, cx, |lines| {
|
||||
let mut seen = HashSet::default();
|
||||
lines.retain(|line| seen.insert(*line));
|
||||
})
|
||||
|
@ -9606,20 +9606,20 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
|
||||
self.manipulate_lines(window, cx, |lines| lines.reverse())
|
||||
}
|
||||
|
||||
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
|
||||
}
|
||||
|
||||
fn manipulate_lines<M>(
|
||||
fn manipulate_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut manipulate: M,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
M: FnMut(&str) -> LineManipulationResult,
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
{
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
|
||||
|
||||
|
@ -9652,14 +9652,18 @@ impl Editor {
|
|||
.text_for_range(start_point..end_point)
|
||||
.collect::<String>();
|
||||
|
||||
let LineManipulationResult { new_text, line_count_before, line_count_after} = manipulate(&text);
|
||||
let mut lines = text.split('\n').collect_vec();
|
||||
|
||||
edits.push((start_point..end_point, new_text));
|
||||
let lines_before = lines.len();
|
||||
callback(&mut lines);
|
||||
let lines_after = lines.len();
|
||||
|
||||
edits.push((start_point..end_point, lines.join("\n")));
|
||||
|
||||
// Selections must change based on added and removed line count
|
||||
let start_row =
|
||||
MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32);
|
||||
let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32);
|
||||
new_selections.push(Selection {
|
||||
id: selection.id,
|
||||
start: start_row,
|
||||
|
@ -9668,10 +9672,10 @@ impl Editor {
|
|||
reversed: selection.reversed,
|
||||
});
|
||||
|
||||
if line_count_after > line_count_before {
|
||||
added_lines += line_count_after - line_count_before;
|
||||
} else if line_count_before > line_count_after {
|
||||
removed_lines += line_count_before - line_count_after;
|
||||
if lines_after > lines_before {
|
||||
added_lines += lines_after - lines_before;
|
||||
} else if lines_before > lines_after {
|
||||
removed_lines += lines_before - lines_after;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9716,171 +9720,6 @@ impl Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn manipulate_immutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<&str>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<&str> = text.split('\n').collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn manipulate_mutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
mut callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<Cow<'_, str>>),
|
||||
{
|
||||
self.manipulate_lines(window, cx, |text| {
|
||||
let mut lines: Vec<Cow<str>> = text.split('\n').map(Cow::from).collect();
|
||||
let line_count_before = lines.len();
|
||||
|
||||
callback(&mut lines);
|
||||
|
||||
LineManipulationResult {
|
||||
new_text: lines.join("\n"),
|
||||
line_count_before,
|
||||
line_count_after: lines.len(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_spaces(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToSpaces,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized scratch buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.as_ref().chars();
|
||||
let mut col = 0;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
reindented_line.push(' ');
|
||||
col += 1;
|
||||
}
|
||||
'\t' => {
|
||||
// \t are converted to spaces depending on the current column
|
||||
let spaces_len = tab_size - (col % tab_size);
|
||||
reindented_line.extend(&space_cache[spaces_len - 1]);
|
||||
col += spaces_len;
|
||||
changed = true;
|
||||
}
|
||||
_ => {
|
||||
// If we dont append before break, the character is consumed
|
||||
reindented_line.push(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_indentation_to_tabs(
|
||||
&mut self,
|
||||
_: &ConvertIndentationToTabs,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = self.buffer.read(cx).language_settings(cx);
|
||||
let tab_size = settings.tab_size.get() as usize;
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, |lines| {
|
||||
// Allocates a reasonably sized buffer once for the whole loop
|
||||
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
|
||||
// Avoids recomputing spaces that could be inserted many times
|
||||
let space_cache: Vec<Vec<char>> = (1..=tab_size)
|
||||
.map(|n| IndentSize::spaces(n as u32).chars().collect())
|
||||
.collect();
|
||||
|
||||
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
|
||||
let mut chars = line.chars();
|
||||
let mut spaces_count = 0;
|
||||
let mut first_non_indent_char = None;
|
||||
let mut changed = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
' ' => {
|
||||
// Keep track of spaces. Append \t when we reach tab_size
|
||||
spaces_count += 1;
|
||||
changed = true;
|
||||
if spaces_count == tab_size {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
}
|
||||
'\t' => {
|
||||
reindented_line.push('\t');
|
||||
spaces_count = 0;
|
||||
}
|
||||
_ => {
|
||||
// Dont append it yet, we might have remaining spaces
|
||||
first_non_indent_char = Some(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
reindented_line.clear();
|
||||
continue;
|
||||
}
|
||||
// Remaining spaces that didn't make a full tab stop
|
||||
if spaces_count > 0 {
|
||||
reindented_line.extend(&space_cache[spaces_count - 1]);
|
||||
}
|
||||
// If we consume an extra character that was not indentation, add it back
|
||||
if let Some(extra_char) = first_non_indent_char {
|
||||
reindented_line.push(extra_char);
|
||||
}
|
||||
// Append the rest of the line and replace old reference with new one
|
||||
reindented_line.extend(chars);
|
||||
*line = Cow::Owned(reindented_line.clone());
|
||||
reindented_line.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_to_upper_case(
|
||||
&mut self,
|
||||
_: &ConvertToUpperCase,
|
||||
|
@ -21318,13 +21157,6 @@ pub struct LineHighlight {
|
|||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
struct LineManipulationResult {
|
||||
pub new_text: String,
|
||||
pub line_count_before: usize,
|
||||
pub line_count_after: usize,
|
||||
}
|
||||
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
|
|
193
crates/assistant_tools/src/project_notifications_tool.rs
Normal file
193
crates/assistant_tools/src/project_notifications_tool.rs
Normal file
|
@ -0,0 +1,193 @@
|
|||
use crate::schema::json_schema_for;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProjectUpdatesToolInput {}
|
||||
|
||||
pub struct ProjectNotificationsTool;
|
||||
|
||||
impl Tool for ProjectNotificationsTool {
|
||||
fn name(&self) -> String {
|
||||
"project_notifications".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn description(&self) -> String {
|
||||
include_str!("./project_notifications_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Envelope
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<ProjectUpdatesToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Check project notifications".into()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let mut stale_files = String::new();
|
||||
|
||||
let action_log = action_log.read(cx);
|
||||
|
||||
for stale_file in action_log.stale_buffers(cx) {
|
||||
if let Some(file) = stale_file.read(cx).file() {
|
||||
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
let response = if stale_files.is_empty() {
|
||||
"No new notifications".to_string()
|
||||
} else {
|
||||
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
|
||||
const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
|
||||
format!("{HEADER}{stale_files}").replace("\r\n", "\n")
|
||||
};
|
||||
|
||||
Task::ready(Ok(response.into())).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::ToolResultContent;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/test"),
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let buffer_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("test/code.rs", cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(buffer_path.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Start tracking the buffer
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
});
|
||||
|
||||
// Run the tool before any changes
|
||||
let tool = Arc::new(ProjectNotificationsTool);
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
|
||||
let request = Arc::new(LanguageModelRequest::default());
|
||||
let tool_input = json!({});
|
||||
|
||||
let result = cx.update(|cx| {
|
||||
tool.clone().run(
|
||||
tool_input.clone(),
|
||||
request.clone(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let response = result.output.await.unwrap();
|
||||
let response_text = match &response.content {
|
||||
ToolResultContent::Text(text) => text.clone(),
|
||||
_ => panic!("Expected text response"),
|
||||
};
|
||||
assert_eq!(
|
||||
response_text.as_str(),
|
||||
"No new notifications",
|
||||
"Tool should return 'No new notifications' when no stale buffers"
|
||||
);
|
||||
|
||||
// Modify the buffer (makes it stale)
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(1..1, "\nChange!\n")], None, cx);
|
||||
});
|
||||
|
||||
// Run the tool again
|
||||
let result = cx.update(|cx| {
|
||||
tool.run(
|
||||
tool_input.clone(),
|
||||
request.clone(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// This time the buffer is stale, so the tool should return a notification
|
||||
let response = result.output.await.unwrap();
|
||||
let response_text = match &response.content {
|
||||
ToolResultContent::Text(text) => text.clone(),
|
||||
_ => panic!("Expected text response"),
|
||||
};
|
||||
|
||||
let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
|
||||
assert_eq!(
|
||||
response_text.as_str(),
|
||||
expected_content,
|
||||
"Tool should return the stale buffer notification"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
assistant_tool::init(cx);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
This tool reports which files have been modified by the user since the agent last accessed them.
|
||||
|
||||
It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates.
|
|
@ -0,0 +1,3 @@
|
|||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
|
@ -1,8 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{RootSchema, Schema, SchemaObject},
|
||||
JsonSchema, Schema,
|
||||
generate::SchemaSettings,
|
||||
transform::{Transform, transform_subschemas},
|
||||
};
|
||||
|
||||
pub fn json_schema_for<T: JsonSchema>(
|
||||
|
@ -13,7 +14,7 @@ pub fn json_schema_for<T: JsonSchema>(
|
|||
}
|
||||
|
||||
fn schema_to_json(
|
||||
schema: &RootSchema,
|
||||
schema: &Schema,
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut value = serde_json::to_value(schema)?;
|
||||
|
@ -21,58 +22,40 @@ fn schema_to_json(
|
|||
Ok(value)
|
||||
}
|
||||
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
let mut generator = match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
schemars::r#gen::SchemaSettings::default()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
settings
|
||||
.visitors
|
||||
.push(Box::new(TransformToJsonSchemaSubsetVisitor));
|
||||
})
|
||||
.into_generator()
|
||||
}
|
||||
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
})
|
||||
.with_transform(ToJsonSchemaSubsetTransform)
|
||||
.into_generator(),
|
||||
};
|
||||
generator.root_schema_for::<T>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransformToJsonSchemaSubsetVisitor;
|
||||
struct ToJsonSchemaSubsetTransform;
|
||||
|
||||
impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
|
||||
fn visit_root_schema(&mut self, root: &mut RootSchema) {
|
||||
schemars::visit::visit_root_schema(self, root)
|
||||
}
|
||||
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
schemars::visit::visit_schema(self, schema)
|
||||
}
|
||||
|
||||
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
|
||||
impl Transform for ToJsonSchemaSubsetTransform {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
// Ensure that the type field is not an array, this happens when we use
|
||||
// Option<T>, the type will be [T, "null"].
|
||||
if let Some(instance_type) = schema.instance_type.take() {
|
||||
schema.instance_type = match instance_type {
|
||||
schemars::schema::SingleOrVec::Single(t) => {
|
||||
Some(schemars::schema::SingleOrVec::Single(t))
|
||||
if let Some(type_field) = schema.get_mut("type") {
|
||||
if let Some(types) = type_field.as_array() {
|
||||
if let Some(first_type) = types.first() {
|
||||
*type_field = first_type.clone();
|
||||
}
|
||||
schemars::schema::SingleOrVec::Vec(items) => items
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(schemars::schema::SingleOrVec::from),
|
||||
};
|
||||
}
|
||||
|
||||
// One of is not supported, use anyOf instead.
|
||||
if let Some(subschema) = schema.subschemas.as_mut() {
|
||||
if let Some(one_of) = subschema.one_of.take() {
|
||||
subschema.any_of = Some(one_of);
|
||||
}
|
||||
}
|
||||
|
||||
schemars::visit::visit_schema_object(self, schema)
|
||||
// oneOf is not supported, use anyOf instead
|
||||
if let Some(one_of) = schema.remove("oneOf") {
|
||||
schema.insert("anyOf".to_string(), one_of);
|
||||
}
|
||||
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ impl Tool for TerminalTool {
|
|||
.update(cx, |project, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
command: program,
|
||||
command: Some(program),
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
|
|
|
@ -28,7 +28,17 @@ use workspace::Workspace;
|
|||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
/// Checks for available updates.
|
||||
Check,
|
||||
/// Dismisses the update error message.
|
||||
DismissErrorMessage,
|
||||
/// Opens the release notes for the current version in a browser.
|
||||
ViewReleaseNotes,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
|
|
|
@ -12,7 +12,13 @@ use workspace::Workspace;
|
|||
use workspace::notifications::simple_message_notification::MessageNotification;
|
||||
use workspace::notifications::{NotificationId, show_app_notification};
|
||||
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
/// Opens the release notes for the current version in a new tab.
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
notify_if_app_was_updated(cx);
|
||||
|
|
|
@ -25,5 +25,4 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, anyhow};
|
||||
use anyhow::{Context, Error, Result, anyhow};
|
||||
use aws_sdk_bedrockruntime as bedrock;
|
||||
pub use aws_sdk_bedrockruntime as bedrock_client;
|
||||
pub use aws_sdk_bedrockruntime::types::{
|
||||
|
@ -24,9 +21,10 @@ pub use bedrock::types::{
|
|||
ToolResultContentBlock as BedrockToolResultContentBlock,
|
||||
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
|
||||
};
|
||||
use futures::stream::{self, BoxStream, Stream};
|
||||
use futures::stream::{self, BoxStream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::models::*;
|
||||
|
@ -34,70 +32,59 @@ pub use crate::models::*;
|
|||
pub async fn stream_completion(
|
||||
client: bedrock::Client,
|
||||
request: Request,
|
||||
handle: tokio::runtime::Handle,
|
||||
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
|
||||
handle
|
||||
.spawn(async move {
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
response =
|
||||
response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
])),
|
||||
)])));
|
||||
}
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
let thinking_config = HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
]);
|
||||
response = response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(thinking_config),
|
||||
)])));
|
||||
}
|
||||
|
||||
if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
if request
|
||||
.tools
|
||||
.as_ref()
|
||||
.map_or(false, |t| !t.tools.is_empty())
|
||||
{
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
|
||||
let response = response.send().await;
|
||||
let output = response
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send API request to Bedrock");
|
||||
|
||||
match response {
|
||||
Ok(output) => {
|
||||
let stream: Pin<
|
||||
Box<
|
||||
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
|
||||
+ Send,
|
||||
>,
|
||||
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some(({ Ok(output) }, stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
Some((
|
||||
// TODO: Figure out how we can capture Throttling Exceptions
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
))
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => Err(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
let stream = Box::pin(stream::unfold(
|
||||
output?.stream,
|
||||
move |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some((Ok(output), stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("spawning a task")?
|
||||
},
|
||||
));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn aws_document_to_value(document: &Document) -> Value {
|
||||
|
|
|
@ -29,7 +29,7 @@ client.workspace = true
|
|||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
|
|
|
@ -12,7 +12,6 @@ pub struct CallSettings {
|
|||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
|
|
|
@ -81,7 +81,17 @@ pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
|
|||
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
actions!(
|
||||
client,
|
||||
[
|
||||
/// Signs in to Zed account.
|
||||
SignIn,
|
||||
/// Signs out of Zed account.
|
||||
SignOut,
|
||||
/// Reconnects to the collaboration server.
|
||||
Reconnect
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ClientSettingsContent {
|
||||
|
|
|
@ -35,6 +35,7 @@ dashmap.workspace = true
|
|||
derive_more.workspace = true
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
hex.workspace = true
|
||||
http_client.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
|
|
|
@ -107,7 +107,7 @@ CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("proj
|
|||
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "project_repositories" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"abs_path" VARCHAR,
|
||||
"id" INTEGER NOT NULL,
|
||||
"entry_ids" VARCHAR,
|
||||
|
@ -124,7 +124,7 @@ CREATE TABLE "project_repositories" (
|
|||
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
|
||||
|
||||
CREATE TABLE "project_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"repository_id" INTEGER NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
DELETE FROM project_repositories
|
||||
WHERE project_id NOT IN (SELECT id FROM projects);
|
||||
|
||||
ALTER TABLE project_repositories
|
||||
ADD CONSTRAINT fk_project_repositories_project_id
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES projects (id)
|
||||
ON DELETE CASCADE
|
||||
NOT VALID;
|
||||
|
||||
ALTER TABLE project_repositories
|
||||
VALIDATE CONSTRAINT fk_project_repositories_project_id;
|
||||
|
||||
DELETE FROM project_repository_statuses
|
||||
WHERE project_id NOT IN (SELECT id FROM projects);
|
||||
|
||||
ALTER TABLE project_repository_statuses
|
||||
ADD CONSTRAINT fk_project_repository_statuses_project_id
|
||||
FOREIGN KEY (project_id)
|
||||
REFERENCES projects (id)
|
||||
ON DELETE CASCADE
|
||||
NOT VALID;
|
||||
|
||||
ALTER TABLE project_repository_statuses
|
||||
VALIDATE CONSTRAINT fk_project_repository_statuses_project_id;
|
|
@ -1404,6 +1404,9 @@ async fn sync_model_request_usage_with_stripe(
|
|||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("Stripe usage sync: Starting");
|
||||
let started_at = Utc::now();
|
||||
|
||||
let staff_users = app.db.get_staff_users().await?;
|
||||
let staff_user_ids = staff_users
|
||||
.iter()
|
||||
|
@ -1448,6 +1451,10 @@ async fn sync_model_request_usage_with_stripe(
|
|||
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
|
||||
.await?;
|
||||
|
||||
let usage_meter_count = usage_meters.len();
|
||||
|
||||
log::info!("Stripe usage sync: Syncing {usage_meter_count} usage meters");
|
||||
|
||||
for (usage_meter, usage) in usage_meters {
|
||||
maybe!(async {
|
||||
let Some((billing_customer, billing_subscription)) =
|
||||
|
@ -1504,5 +1511,10 @@ async fn sync_model_request_usage_with_stripe(
|
|||
.log_err();
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Stripe usage sync: Synced {usage_meter_count} usage meters in {:?}",
|
||||
Utc::now() - started_at
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,20 +4,19 @@ mod tables;
|
|||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
use crate::{Error, Result, executor::Executor};
|
||||
use crate::{Error, Result};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use project_repository_statuses::StatusKind;
|
||||
use rand::{Rng, SeedableRng, prelude::StdRng};
|
||||
use rpc::ExtensionProvides;
|
||||
use rpc::{
|
||||
ConnectionId, ExtensionMetadata,
|
||||
proto::{self},
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
|
||||
ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
|
||||
FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
|
||||
TransactionTrait,
|
||||
entity::prelude::*,
|
||||
|
@ -33,7 +32,6 @@ use std::{
|
|||
ops::{Deref, DerefMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use time::PrimitiveDateTime;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
@ -58,6 +56,7 @@ pub use tables::*;
|
|||
|
||||
#[cfg(test)]
|
||||
pub struct DatabaseTestOptions {
|
||||
pub executor: gpui::BackgroundExecutor,
|
||||
pub runtime: tokio::runtime::Runtime,
|
||||
pub query_failure_probability: parking_lot::Mutex<f64>,
|
||||
}
|
||||
|
@ -69,8 +68,6 @@ pub struct Database {
|
|||
pool: DatabaseConnection,
|
||||
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
|
||||
projects: DashMap<ProjectId, Arc<Mutex<()>>>,
|
||||
rng: Mutex<StdRng>,
|
||||
executor: Executor,
|
||||
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
|
||||
notification_kinds_by_name: HashMap<String, NotificationKindId>,
|
||||
#[cfg(test)]
|
||||
|
@ -81,17 +78,15 @@ pub struct Database {
|
|||
// separate files in the `queries` folder.
|
||||
impl Database {
|
||||
/// Connects to the database with the given options
|
||||
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
|
||||
pub async fn new(options: ConnectOptions) -> Result<Self> {
|
||||
sqlx::any::install_default_drivers();
|
||||
Ok(Self {
|
||||
options: options.clone(),
|
||||
pool: sea_orm::Database::connect(options).await?,
|
||||
rooms: DashMap::with_capacity(16384),
|
||||
projects: DashMap::with_capacity(16384),
|
||||
rng: Mutex::new(StdRng::seed_from_u64(0)),
|
||||
notification_kinds_by_id: HashMap::default(),
|
||||
notification_kinds_by_name: HashMap::default(),
|
||||
executor,
|
||||
#[cfg(test)]
|
||||
test_options: None,
|
||||
})
|
||||
|
@ -107,48 +102,13 @@ impl Database {
|
|||
self.projects.clear();
|
||||
}
|
||||
|
||||
/// Transaction runs things in a transaction. If you want to call other methods
|
||||
/// and pass the transaction around you need to reborrow the transaction at each
|
||||
/// call site with: `&*tx`.
|
||||
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let body = async {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(result) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => return Ok(result),
|
||||
Err(error) => {
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
};
|
||||
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let body = async {
|
||||
let (tx, result) = self.with_weak_transaction(&f).await?;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(result) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => Ok(result),
|
||||
|
@ -174,44 +134,28 @@ impl Database {
|
|||
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
|
||||
{
|
||||
let body = async {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(Some((room_id, data))) => {
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
}));
|
||||
}
|
||||
Err(error) => {
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => return Ok(None),
|
||||
Err(error) => {
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(Some((room_id, data))) => {
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => Ok(Some(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
})),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
Ok(None) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -229,38 +173,26 @@ impl Database {
|
|||
{
|
||||
let room_id = Database::room_id_for_project(self, project_id).await?;
|
||||
let body = async {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let lock = if let Some(room_id) = room_id {
|
||||
self.rooms.entry(room_id).or_default().clone()
|
||||
} else {
|
||||
self.projects.entry(project_id).or_default().clone()
|
||||
};
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => {
|
||||
return Ok(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
let lock = if let Some(room_id) = room_id {
|
||||
self.rooms.entry(room_id).or_default().clone()
|
||||
} else {
|
||||
self.projects.entry(project_id).or_default().clone()
|
||||
};
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => Ok(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
}),
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
Err(error)
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -280,34 +212,22 @@ impl Database {
|
|||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let body = async {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => {
|
||||
return Ok(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if !self.retry_on_serialization_error(&error, i).await {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => Ok(TransactionGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
}),
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
Err(error)
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -315,28 +235,6 @@ impl Database {
|
|||
}
|
||||
|
||||
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let tx = self
|
||||
.pool
|
||||
.begin_with_config(Some(IsolationLevel::Serializable), None)
|
||||
.await?;
|
||||
|
||||
let mut tx = Arc::new(Some(tx));
|
||||
let result = f(TransactionHandle(tx.clone())).await;
|
||||
let tx = Arc::get_mut(&mut tx)
|
||||
.and_then(|tx| tx.take())
|
||||
.context("couldn't complete transaction because it's still in use")?;
|
||||
|
||||
Ok((tx, result))
|
||||
}
|
||||
|
||||
async fn with_weak_transaction<F, Fut, T>(
|
||||
&self,
|
||||
f: &F,
|
||||
) -> Result<(DatabaseTransaction, Result<T>)>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
|
@ -361,13 +259,13 @@ impl Database {
|
|||
{
|
||||
#[cfg(test)]
|
||||
{
|
||||
use rand::prelude::*;
|
||||
|
||||
let test_options = self.test_options.as_ref().unwrap();
|
||||
if let Executor::Deterministic(executor) = &self.executor {
|
||||
executor.simulate_random_delay().await;
|
||||
let fail_probability = *test_options.query_failure_probability.lock();
|
||||
if executor.rng().gen_bool(fail_probability) {
|
||||
return Err(anyhow!("simulated query failure"))?;
|
||||
}
|
||||
test_options.executor.simulate_random_delay().await;
|
||||
let fail_probability = *test_options.query_failure_probability.lock();
|
||||
if test_options.executor.rng().gen_bool(fail_probability) {
|
||||
return Err(anyhow!("simulated query failure"))?;
|
||||
}
|
||||
|
||||
test_options.runtime.block_on(future)
|
||||
|
@ -378,46 +276,6 @@ impl Database {
|
|||
future.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
|
||||
// If the error is due to a failure to serialize concurrent transactions, then retry
|
||||
// this transaction after a delay. With each subsequent retry, double the delay duration.
|
||||
// Also vary the delay randomly in order to ensure different database connections retry
|
||||
// at different times.
|
||||
const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
|
||||
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
|
||||
let base_delay = SLEEPS[prev_attempt_count];
|
||||
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
log::warn!(
|
||||
"retrying transaction after serialization error. delay: {} ms.",
|
||||
randomized_delay
|
||||
);
|
||||
self.executor
|
||||
.sleep(Duration::from_millis(randomized_delay as u64))
|
||||
.await;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_serialization_error(error: &Error) -> bool {
|
||||
const SERIALIZATION_FAILURE_CODE: &str = "40001";
|
||||
match error {
|
||||
Error::Database(
|
||||
DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error))
|
||||
| DbErr::Query(sea_orm::RuntimeErr::SqlxError(error)),
|
||||
) if error
|
||||
.as_database_error()
|
||||
.and_then(|error| error.code())
|
||||
.as_deref()
|
||||
== Some(SERIALIZATION_FAILURE_CODE) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a [`DatabaseTransaction`].
|
||||
|
|
|
@ -20,7 +20,7 @@ impl Database {
|
|||
&self,
|
||||
params: &CreateBillingCustomerParams,
|
||||
) -> Result<billing_customer::Model> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let customer = billing_customer::Entity::insert(billing_customer::ActiveModel {
|
||||
user_id: ActiveValue::set(params.user_id),
|
||||
stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()),
|
||||
|
@ -40,7 +40,7 @@ impl Database {
|
|||
id: BillingCustomerId,
|
||||
params: &UpdateBillingCustomerParams,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
billing_customer::Entity::update(billing_customer::ActiveModel {
|
||||
id: ActiveValue::set(id),
|
||||
user_id: params.user_id.clone(),
|
||||
|
@ -61,7 +61,7 @@ impl Database {
|
|||
&self,
|
||||
id: BillingCustomerId,
|
||||
) -> Result<Option<billing_customer::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_customer::Entity::find()
|
||||
.filter(billing_customer::Column::Id.eq(id))
|
||||
.one(&*tx)
|
||||
|
@ -75,7 +75,7 @@ impl Database {
|
|||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<billing_customer::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_customer::Entity::find()
|
||||
.filter(billing_customer::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
|
@ -89,7 +89,7 @@ impl Database {
|
|||
&self,
|
||||
stripe_customer_id: &str,
|
||||
) -> Result<Option<billing_customer::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_customer::Entity::find()
|
||||
.filter(billing_customer::Column::StripeCustomerId.eq(stripe_customer_id))
|
||||
.one(&*tx)
|
||||
|
|
|
@ -22,7 +22,7 @@ impl Database {
|
|||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<billing_preference::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_preference::Entity::find()
|
||||
.filter(billing_preference::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
|
@ -37,7 +37,7 @@ impl Database {
|
|||
user_id: UserId,
|
||||
params: &CreateBillingPreferencesParams,
|
||||
) -> Result<billing_preference::Model> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let preferences = billing_preference::Entity::insert(billing_preference::ActiveModel {
|
||||
user_id: ActiveValue::set(user_id),
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
|
@ -65,7 +65,7 @@ impl Database {
|
|||
user_id: UserId,
|
||||
params: &UpdateBillingPreferencesParams,
|
||||
) -> Result<billing_preference::Model> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let preferences = billing_preference::Entity::update_many()
|
||||
.set(billing_preference::ActiveModel {
|
||||
max_monthly_llm_usage_spending_in_cents: params
|
||||
|
|
|
@ -35,7 +35,7 @@ impl Database {
|
|||
&self,
|
||||
params: &CreateBillingSubscriptionParams,
|
||||
) -> Result<billing_subscription::Model> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel {
|
||||
billing_customer_id: ActiveValue::set(params.billing_customer_id),
|
||||
kind: ActiveValue::set(params.kind),
|
||||
|
@ -64,7 +64,7 @@ impl Database {
|
|||
id: BillingSubscriptionId,
|
||||
params: &UpdateBillingSubscriptionParams,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
billing_subscription::Entity::update(billing_subscription::ActiveModel {
|
||||
id: ActiveValue::set(id),
|
||||
billing_customer_id: params.billing_customer_id.clone(),
|
||||
|
@ -90,7 +90,7 @@ impl Database {
|
|||
&self,
|
||||
id: BillingSubscriptionId,
|
||||
) -> Result<Option<billing_subscription::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_subscription::Entity::find_by_id(id)
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
|
@ -103,7 +103,7 @@ impl Database {
|
|||
&self,
|
||||
stripe_subscription_id: &str,
|
||||
) -> Result<Option<billing_subscription::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_subscription::Entity::find()
|
||||
.filter(
|
||||
billing_subscription::Column::StripeSubscriptionId.eq(stripe_subscription_id),
|
||||
|
@ -118,7 +118,7 @@ impl Database {
|
|||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<billing_subscription::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(billing_customer::Column::UserId.eq(user_id))
|
||||
|
@ -152,7 +152,7 @@ impl Database {
|
|||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Vec<billing_subscription::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let subscriptions = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(billing_customer::Column::UserId.eq(user_id))
|
||||
|
@ -169,7 +169,7 @@ impl Database {
|
|||
&self,
|
||||
user_ids: HashSet<UserId>,
|
||||
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
|
||||
self.weak_transaction(|tx| {
|
||||
self.transaction(|tx| {
|
||||
let user_ids = user_ids.clone();
|
||||
async move {
|
||||
let mut rows = billing_subscription::Entity::find()
|
||||
|
@ -201,7 +201,7 @@ impl Database {
|
|||
&self,
|
||||
user_ids: HashSet<UserId>,
|
||||
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
|
||||
self.weak_transaction(|tx| {
|
||||
self.transaction(|tx| {
|
||||
let user_ids = user_ids.clone();
|
||||
async move {
|
||||
let mut rows = billing_subscription::Entity::find()
|
||||
|
@ -236,7 +236,7 @@ impl Database {
|
|||
|
||||
/// Returns the count of the active billing subscriptions for the user with the specified ID.
|
||||
pub async fn count_active_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let count = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(
|
||||
|
|
|
@ -501,10 +501,8 @@ impl Database {
|
|||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.weak_transaction(
|
||||
|tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
|
||||
)
|
||||
.await
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
|
|
|
@ -15,7 +15,7 @@ impl Database {
|
|||
user_b_busy: bool,
|
||||
}
|
||||
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let user_a_participant = Alias::new("user_a_participant");
|
||||
let user_b_participant = Alias::new("user_b_participant");
|
||||
let mut db_contacts = contact::Entity::find()
|
||||
|
@ -91,7 +91,7 @@ impl Database {
|
|||
|
||||
/// Returns whether the given user is a busy (on a call).
|
||||
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
|
|
|
@ -9,7 +9,7 @@ pub enum ContributorSelector {
|
|||
impl Database {
|
||||
/// Retrieves the GitHub logins of all users who have signed the CLA.
|
||||
pub async fn get_contributors(&self) -> Result<Vec<String>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryGithubLogin {
|
||||
GithubLogin,
|
||||
|
@ -32,7 +32,7 @@ impl Database {
|
|||
&self,
|
||||
selector: &ContributorSelector,
|
||||
) -> Result<Option<DateTime>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let condition = match selector {
|
||||
ContributorSelector::GitHubUserId { github_user_id } => {
|
||||
user::Column::GithubUserId.eq(*github_user_id)
|
||||
|
@ -69,7 +69,7 @@ impl Database {
|
|||
github_user_created_at: DateTimeUtc,
|
||||
initial_channel_id: Option<ChannelId>,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let user = self
|
||||
.update_or_create_user_by_github_account_tx(
|
||||
github_login,
|
||||
|
|
|
@ -8,7 +8,7 @@ impl Database {
|
|||
model: &str,
|
||||
digests: &[Vec<u8>],
|
||||
) -> Result<HashMap<Vec<u8>, Vec<f32>>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let embeddings = {
|
||||
let mut db_embeddings = embedding::Entity::find()
|
||||
.filter(
|
||||
|
@ -52,7 +52,7 @@ impl Database {
|
|||
model: &str,
|
||||
embeddings: &HashMap<Vec<u8>, Vec<f32>>,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
embedding::Entity::insert_many(embeddings.iter().map(|(digest, dimensions)| {
|
||||
let now_offset_datetime = OffsetDateTime::now_utc();
|
||||
let retrieved_at =
|
||||
|
@ -78,7 +78,7 @@ impl Database {
|
|||
}
|
||||
|
||||
pub async fn purge_old_embeddings(&self) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
embedding::Entity::delete_many()
|
||||
.filter(
|
||||
embedding::Column::RetrievedAt
|
||||
|
|
|
@ -15,7 +15,7 @@ impl Database {
|
|||
max_schema_version: i32,
|
||||
limit: usize,
|
||||
) -> Result<Vec<ExtensionMetadata>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let mut condition = Condition::all()
|
||||
.add(
|
||||
extension::Column::LatestVersion
|
||||
|
@ -43,7 +43,7 @@ impl Database {
|
|||
ids: &[&str],
|
||||
constraints: Option<&ExtensionVersionConstraints>,
|
||||
) -> Result<Vec<ExtensionMetadata>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let extensions = extension::Entity::find()
|
||||
.filter(extension::Column::ExternalId.is_in(ids.iter().copied()))
|
||||
.all(&*tx)
|
||||
|
@ -123,7 +123,7 @@ impl Database {
|
|||
&self,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ExtensionMetadata>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let condition = extension::Column::ExternalId
|
||||
.eq(extension_id)
|
||||
.into_condition();
|
||||
|
@ -162,7 +162,7 @@ impl Database {
|
|||
extension_id: &str,
|
||||
constraints: Option<&ExtensionVersionConstraints>,
|
||||
) -> Result<Option<ExtensionMetadata>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let extension = extension::Entity::find()
|
||||
.filter(extension::Column::ExternalId.eq(extension_id))
|
||||
.one(&*tx)
|
||||
|
@ -187,7 +187,7 @@ impl Database {
|
|||
extension_id: &str,
|
||||
version: &str,
|
||||
) -> Result<Option<ExtensionMetadata>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let extension = extension::Entity::find()
|
||||
.filter(extension::Column::ExternalId.eq(extension_id))
|
||||
.filter(extension_version::Column::Version.eq(version))
|
||||
|
@ -204,7 +204,7 @@ impl Database {
|
|||
}
|
||||
|
||||
pub async fn get_known_extension_versions(&self) -> Result<HashMap<String, Vec<String>>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let mut extension_external_ids_by_id = HashMap::default();
|
||||
|
||||
let mut rows = extension::Entity::find().stream(&*tx).await?;
|
||||
|
@ -242,7 +242,7 @@ impl Database {
|
|||
&self,
|
||||
versions_by_extension_id: &HashMap<&str, Vec<NewExtensionVersion>>,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
for (external_id, versions) in versions_by_extension_id {
|
||||
if versions.is_empty() {
|
||||
continue;
|
||||
|
@ -349,7 +349,7 @@ impl Database {
|
|||
}
|
||||
|
||||
pub async fn record_extension_download(&self, extension: &str, version: &str) -> Result<bool> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryId {
|
||||
Id,
|
||||
|
|
|
@ -13,7 +13,7 @@ impl Database {
|
|||
&self,
|
||||
params: &CreateProcessedStripeEventParams,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
processed_stripe_event::Entity::insert(processed_stripe_event::ActiveModel {
|
||||
stripe_event_id: ActiveValue::set(params.stripe_event_id.clone()),
|
||||
stripe_event_type: ActiveValue::set(params.stripe_event_type.clone()),
|
||||
|
@ -35,7 +35,7 @@ impl Database {
|
|||
&self,
|
||||
event_id: &str,
|
||||
) -> Result<Option<processed_stripe_event::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(processed_stripe_event::Entity::find_by_id(event_id)
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
|
@ -48,7 +48,7 @@ impl Database {
|
|||
&self,
|
||||
event_ids: &[&str],
|
||||
) -> Result<Vec<processed_stripe_event::Model>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(processed_stripe_event::Entity::find()
|
||||
.filter(
|
||||
processed_stripe_event::Column::StripeEventId.is_in(event_ids.iter().copied()),
|
||||
|
|
|
@ -112,7 +112,7 @@ impl Database {
|
|||
}
|
||||
|
||||
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
project::Entity::delete_by_id(project_id).exec(&*tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
|
|
|
@ -80,7 +80,7 @@ impl Database {
|
|||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<proto::IncomingCall>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let pending_participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::UserId
|
||||
|
|
|
@ -142,6 +142,50 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let delete_query = Query::delete()
|
||||
.from_table(project_repository_statuses::Entity)
|
||||
.and_where(
|
||||
Expr::tuple([Expr::col((
|
||||
project_repository_statuses::Entity,
|
||||
project_repository_statuses::Column::ProjectId,
|
||||
))
|
||||
.into()])
|
||||
.in_subquery(
|
||||
Query::select()
|
||||
.columns([(
|
||||
project_repository_statuses::Entity,
|
||||
project_repository_statuses::Column::ProjectId,
|
||||
)])
|
||||
.from(project_repository_statuses::Entity)
|
||||
.inner_join(
|
||||
project::Entity,
|
||||
Expr::col((project::Entity, project::Column::Id)).equals((
|
||||
project_repository_statuses::Entity,
|
||||
project_repository_statuses::Column::ProjectId,
|
||||
)),
|
||||
)
|
||||
.and_where(project::Column::HostConnectionServerId.ne(server_id))
|
||||
.limit(10000)
|
||||
.to_owned(),
|
||||
),
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
let statement = Statement::from_sql_and_values(
|
||||
tx.get_database_backend(),
|
||||
delete_query
|
||||
.to_string(sea_orm::sea_query::PostgresQueryBuilder)
|
||||
.as_str(),
|
||||
vec![],
|
||||
);
|
||||
|
||||
let result = tx.execute(statement).await?;
|
||||
if result.rows_affected() == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -382,7 +382,7 @@ impl Database {
|
|||
|
||||
/// Returns the active flags for the user.
|
||||
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryAs {
|
||||
Flag,
|
||||
|
|
|
@ -17,11 +17,15 @@ use crate::migrations::run_database_migrations;
|
|||
use super::*;
|
||||
use gpui::BackgroundExecutor;
|
||||
use parking_lot::Mutex;
|
||||
use rand::prelude::*;
|
||||
use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicI32, AtomicU32, Ordering::SeqCst},
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicI32, AtomicU32, Ordering::SeqCst},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct TestDb {
|
||||
|
@ -41,9 +45,7 @@ impl TestDb {
|
|||
let mut db = runtime.block_on(async {
|
||||
let mut options = ConnectOptions::new(url);
|
||||
options.max_connections(5);
|
||||
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut db = Database::new(options).await.unwrap();
|
||||
let sql = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/migrations.sqlite/20221109000000_test_schema.sql"
|
||||
|
@ -60,6 +62,7 @@ impl TestDb {
|
|||
});
|
||||
|
||||
db.test_options = Some(DatabaseTestOptions {
|
||||
executor,
|
||||
runtime,
|
||||
query_failure_probability: parking_lot::Mutex::new(0.0),
|
||||
});
|
||||
|
@ -93,9 +96,7 @@ impl TestDb {
|
|||
options
|
||||
.max_connections(5)
|
||||
.idle_timeout(Duration::from_secs(0));
|
||||
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut db = Database::new(options).await.unwrap();
|
||||
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
|
||||
run_database_migrations(db.options(), migrations_path)
|
||||
.await
|
||||
|
@ -105,6 +106,7 @@ impl TestDb {
|
|||
});
|
||||
|
||||
db.test_options = Some(DatabaseTestOptions {
|
||||
executor,
|
||||
runtime,
|
||||
query_failure_probability: parking_lot::Mutex::new(0.0),
|
||||
});
|
||||
|
|
|
@ -49,7 +49,7 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
|
|||
db.save_embeddings(model, &embeddings).await.unwrap();
|
||||
|
||||
// Reach into the DB and change the retrieved at to be > 60 days
|
||||
db.weak_transaction(|tx| {
|
||||
db.transaction(|tx| {
|
||||
let digest = digest.clone();
|
||||
async move {
|
||||
let sixty_days_ago = OffsetDateTime::now_utc().sub(Duration::days(61));
|
||||
|
|
|
@ -285,7 +285,7 @@ impl AppState {
|
|||
pub async fn new(config: Config, executor: Executor) -> Result<Arc<Self>> {
|
||||
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
db_options.max_connections(config.database_max_connections);
|
||||
let mut db = Database::new(db_options, Executor::Production).await?;
|
||||
let mut db = Database::new(db_options).await?;
|
||||
db.initialize_notification_kinds().await?;
|
||||
|
||||
let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config
|
||||
|
|
|
@ -59,7 +59,7 @@ async fn main() -> Result<()> {
|
|||
let config = envy::from_env::<Config>().expect("error loading config");
|
||||
let db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
|
||||
let mut db = Database::new(db_options, Executor::Production).await?;
|
||||
let mut db = Database::new(db_options).await?;
|
||||
db.initialize_notification_kinds().await?;
|
||||
|
||||
collab::seed::seed(&config, &db, false).await?;
|
||||
|
@ -253,7 +253,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
async fn setup_app_database(config: &Config) -> Result<()> {
|
||||
let db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
let mut db = Database::new(db_options, Executor::Production).await?;
|
||||
let mut db = Database::new(db_options).await?;
|
||||
|
||||
let migrations_path = config.migrations_path.as_deref().unwrap_or_else(|| {
|
||||
#[cfg(feature = "sqlite")]
|
||||
|
|
|
@ -4591,14 +4591,13 @@ async fn test_formatting_buffer(
|
|||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::External {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: Some(
|
||||
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
),
|
||||
}]
|
||||
.into(),
|
||||
},
|
||||
)));
|
||||
});
|
||||
});
|
||||
|
@ -4699,8 +4698,8 @@ async fn test_prettier_formatting_buffer(
|
|||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
|
@ -4822,7 +4821,7 @@ async fn test_definition(
|
|||
);
|
||||
|
||||
let definitions_1 = project_b
|
||||
.update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
|
@ -4853,7 +4852,7 @@ async fn test_definition(
|
|||
);
|
||||
|
||||
let definitions_2 = project_b
|
||||
.update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
|
@ -4890,7 +4889,7 @@ async fn test_definition(
|
|||
);
|
||||
|
||||
let type_definitions = project_b
|
||||
.update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
|
||||
.update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
|
@ -5058,7 +5057,7 @@ async fn test_references(
|
|||
lsp_response_tx
|
||||
.unbounded_send(Err(anyhow!("can't find references")))
|
||||
.unwrap();
|
||||
references.await.unwrap_err();
|
||||
assert_eq!(references.await.unwrap(), []);
|
||||
|
||||
// User is informed that the request is no longer pending.
|
||||
executor.run_until_parked();
|
||||
|
@ -5642,7 +5641,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
|||
let definitions;
|
||||
let buffer_b2;
|
||||
if rng.r#gen() {
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
|
@ -5656,7 +5655,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
|||
})
|
||||
.await
|
||||
.unwrap();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let definitions = definitions.await.unwrap();
|
||||
|
|
|
@ -838,7 +838,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
.map(|_| Ok(()))
|
||||
.boxed(),
|
||||
LspRequestKind::Definition => project
|
||||
.definition(&buffer, offset, cx)
|
||||
.definitions(&buffer, offset, cx)
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::Highlights => project
|
||||
|
|
|
@ -505,8 +505,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
|||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
|
|
|
@ -30,7 +30,13 @@ use workspace::{
|
|||
};
|
||||
use workspace::{item::Dedup, notifications::NotificationId};
|
||||
|
||||
actions!(collab, [CopyLink]);
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
/// Copies a link to the current position in the channel buffer.
|
||||
CopyLink
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
|
||||
|
|
|
@ -71,7 +71,13 @@ struct SerializedChatPanel {
|
|||
width: Option<Pixels>,
|
||||
}
|
||||
|
||||
actions!(chat_panel, [ToggleFocus]);
|
||||
actions!(
|
||||
chat_panel,
|
||||
[
|
||||
/// Toggles focus on the chat panel.
|
||||
ToggleFocus
|
||||
]
|
||||
);
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(
|
||||
|
|
|
@ -44,15 +44,25 @@ use workspace::{
|
|||
actions!(
|
||||
collab_panel,
|
||||
[
|
||||
/// Toggles focus on the collaboration panel.
|
||||
ToggleFocus,
|
||||
/// Removes the selected channel or contact.
|
||||
Remove,
|
||||
/// Opens the context menu for the selected item.
|
||||
Secondary,
|
||||
/// Collapses the selected channel in the tree view.
|
||||
CollapseSelectedChannel,
|
||||
/// Expands the selected channel in the tree view.
|
||||
ExpandSelectedChannel,
|
||||
/// Starts moving a channel to a new location.
|
||||
StartMoveChannel,
|
||||
/// Moves the selected item to the current location.
|
||||
MoveSelected,
|
||||
/// Inserts a space character in the filter input.
|
||||
InsertSpace,
|
||||
/// Moves the selected channel up in the list.
|
||||
MoveChannelUp,
|
||||
/// Moves the selected channel down in the list.
|
||||
MoveChannelDown,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -17,9 +17,13 @@ use workspace::{ModalView, notifications::DetachAndPromptErr};
|
|||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
/// Selects the next control in the channel modal.
|
||||
SelectNextControl,
|
||||
/// Toggles between invite members and manage members mode.
|
||||
ToggleMode,
|
||||
/// Toggles admin status for the selected member.
|
||||
ToggleMemberAdmin,
|
||||
/// Removes the selected member from the channel.
|
||||
RemoveMember
|
||||
]
|
||||
);
|
||||
|
|
|
@ -74,7 +74,13 @@ pub struct NotificationPresenter {
|
|||
pub can_navigate: bool,
|
||||
}
|
||||
|
||||
actions!(notification_panel, [ToggleFocus]);
|
||||
actions!(
|
||||
notification_panel,
|
||||
[
|
||||
/// Toggles focus on the notification panel.
|
||||
ToggleFocus
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
|
|
|
@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
|
@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
|
@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
|
|
|
@ -41,7 +41,7 @@ pub struct CommandPalette {
|
|||
/// Removes subsequent whitespace characters and double colons from the query.
|
||||
///
|
||||
/// This improves the likelihood of a match by either humanized name or keymap-style name.
|
||||
fn normalize_query(input: &str) -> String {
|
||||
pub fn normalize_action_query(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut last_char = None;
|
||||
|
||||
|
@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
let mut commands = self.all_commands.clone();
|
||||
let hit_counts = self.hit_counts();
|
||||
let executor = cx.background_executor().clone();
|
||||
let query = normalize_query(query.as_str());
|
||||
let query = normalize_action_query(query.as_str());
|
||||
async move {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
|
@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
tx.send((commands, matches)).await.log_err();
|
||||
}
|
||||
|
@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
let matching_command = self.matches.get(ix)?;
|
||||
let command = self.commands.get(matching_command.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
|
@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
matching_command.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
|
@ -512,19 +500,28 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_normalize_query() {
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(
|
||||
normalize_query("editor::GoToDefinition"),
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor::::GoToDefinition"),
|
||||
normalize_action_query("editor::::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor: :GoToDefinition"),
|
||||
normalize_action_query("editor: :GoToDefinition"),
|
||||
"editor: :GoToDefinition"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ impl RenderOnce for ComponentExample {
|
|||
12.0,
|
||||
12.0,
|
||||
))
|
||||
.shadow_sm()
|
||||
.shadow_xs()
|
||||
.child(self.element),
|
||||
)
|
||||
.into_any_element()
|
||||
|
|
|
@ -46,11 +46,17 @@ pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_an
|
|||
actions!(
|
||||
copilot,
|
||||
[
|
||||
/// Requests a code completion suggestion from Copilot.
|
||||
Suggest,
|
||||
/// Cycles to the next Copilot suggestion.
|
||||
NextSuggestion,
|
||||
/// Cycles to the previous Copilot suggestion.
|
||||
PreviousSuggestion,
|
||||
/// Reinstalls the Copilot language server.
|
||||
Reinstall,
|
||||
/// Signs in to GitHub Copilot.
|
||||
SignIn,
|
||||
/// Signs out of GitHub Copilot.
|
||||
SignOut
|
||||
]
|
||||
);
|
||||
|
|
|
@ -528,6 +528,7 @@ impl CopilotChat {
|
|||
|
||||
pub async fn stream_completion(
|
||||
request: Request,
|
||||
is_user_initiated: bool,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let this = cx
|
||||
|
@ -562,7 +563,14 @@ impl CopilotChat {
|
|||
};
|
||||
|
||||
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
|
||||
stream_completion(client.clone(), token.api_key, api_url.into(), request).await
|
||||
stream_completion(
|
||||
client.clone(),
|
||||
token.api_key,
|
||||
api_url.into(),
|
||||
request,
|
||||
is_user_initiated,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn set_configuration(
|
||||
|
@ -697,6 +705,7 @@ async fn stream_completion(
|
|||
api_key: String,
|
||||
completion_url: Arc<str>,
|
||||
request: Request,
|
||||
is_user_initiated: bool,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let is_vision_request = request.messages.iter().any(|message| match message {
|
||||
ChatMessage::User { content }
|
||||
|
@ -707,6 +716,8 @@ async fn stream_completion(
|
|||
_ => false,
|
||||
});
|
||||
|
||||
let request_initiator = if is_user_initiated { "user" } else { "agent" };
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(completion_url.as_ref())
|
||||
|
@ -719,7 +730,8 @@ async fn stream_completion(
|
|||
)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("X-Initiator", request_initiator);
|
||||
|
||||
if is_vision_request {
|
||||
request_builder =
|
||||
|
|
|
@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
|
|||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
use language::{LanguageName, LanguageToolchainStore};
|
||||
use node_runtime::NodeRuntime;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::fs::File;
|
||||
|
@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
|
|||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct DebugAdapterName(pub SharedString);
|
||||
|
||||
impl Deref for DebugAdapterName {
|
||||
|
|
|
@ -25,7 +25,9 @@ anyhow.workspace = true
|
|||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
dotenvy.workspace = true
|
||||
futures.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
json_dotpath.workspace = true
|
||||
language.workspace = true
|
||||
|
|
|
@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
|
|||
async fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
mut configuration: Value,
|
||||
label: &str,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
let mut configuration = task_definition.config.clone();
|
||||
|
||||
let obj = configuration
|
||||
.as_object_mut()
|
||||
.context("CodeLLDB is not a valid json object")?;
|
||||
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
obj.entry("name")
|
||||
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
|
||||
.or_insert(Value::String(String::from(label)));
|
||||
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
|||
self.path_to_codelldb.set(path.clone()).ok();
|
||||
command = Some(path);
|
||||
};
|
||||
|
||||
let mut json_config = config.config.clone();
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
arguments: user_args.unwrap_or_else(|| {
|
||||
vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
]
|
||||
if let Some(config) = json_config.as_object_mut()
|
||||
&& let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
|
||||
value
|
||||
.as_array()
|
||||
.map_or(false, |array| array.iter().all(Value::is_string))
|
||||
})
|
||||
{
|
||||
let ret = vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": source_languages}).to_string(),
|
||||
];
|
||||
config.remove("sourceLanguages");
|
||||
ret
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}),
|
||||
request_args: self.request_args(delegate, &config).await?,
|
||||
request_args: self
|
||||
.request_args(delegate, json_config, &config.label)
|
||||
.await?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
|
|
@ -7,13 +7,22 @@ use dap::{
|
|||
latest_github_release,
|
||||
},
|
||||
};
|
||||
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use log::warn;
|
||||
use serde_json::{Map, Value};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util;
|
||||
|
||||
use std::{
|
||||
env::consts,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
|
|||
adapter_path.join("dlv").to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
let cwd = task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
|
||||
let cwd = Some(
|
||||
task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
|
||||
);
|
||||
|
||||
let arguments;
|
||||
let command;
|
||||
let connection;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
let mut envs = HashMap::default();
|
||||
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
handle_envs(
|
||||
configuration,
|
||||
&mut envs,
|
||||
cwd.as_deref(),
|
||||
delegate.fs().clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(connection_options) = &task_definition.tcp_connection {
|
||||
|
@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
|
|||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
cwd,
|
||||
envs,
|
||||
connection,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
|
@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// delve doesn't do anything with the envFile setting, so we intercept it
|
||||
async fn handle_envs(
|
||||
config: &mut Map<String, Value>,
|
||||
envs: &mut HashMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> Option<()> {
|
||||
let env_files = match config.get("envFile")? {
|
||||
Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
|
||||
Value::String(s) => vec![Some(s.as_str())],
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rebase_path = |path: PathBuf| {
|
||||
if path.is_absolute() {
|
||||
Some(path)
|
||||
} else {
|
||||
cwd.map(|p| p.join(path))
|
||||
}
|
||||
};
|
||||
|
||||
for path in env_files {
|
||||
let Some(path) = path
|
||||
.and_then(|s| PathBuf::from_str(s).ok())
|
||||
.and_then(rebase_path)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(file) = fs.open_sync(&path).await {
|
||||
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
|
||||
} else {
|
||||
warn!("While starting Go debug session: failed to read env file {path:?}");
|
||||
};
|
||||
}
|
||||
|
||||
// remove envFile now that it's been handled
|
||||
config.remove("entry");
|
||||
Some(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use adapters::latest_github_release;
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::{ResultExt, maybe};
|
||||
|
||||
|
@ -70,6 +71,8 @@ impl JsDebugAdapter {
|
|||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut envs = HashMap::default();
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
maybe!({
|
||||
|
@ -79,9 +82,9 @@ impl JsDebugAdapter {
|
|||
let command = configuration.get("command")?.as_str()?.to_owned();
|
||||
let mut args = shlex::split(&command)?.into_iter();
|
||||
let program = args.next()?;
|
||||
configuration.insert("program".to_owned(), program.into());
|
||||
configuration.insert("runtimeExecutable".to_owned(), program.into());
|
||||
configuration.insert(
|
||||
"args".to_owned(),
|
||||
"runtimeArgs".to_owned(),
|
||||
args.map(Value::from).collect::<Vec<_>>().into(),
|
||||
);
|
||||
configuration.insert("console".to_owned(), "externalTerminal".into());
|
||||
|
@ -110,6 +113,12 @@ impl JsDebugAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(env) = configuration.get("env").cloned() {
|
||||
if let Ok(env) = serde_json::from_value(env) {
|
||||
envs = env;
|
||||
}
|
||||
}
|
||||
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
@ -158,7 +167,7 @@ impl JsDebugAdapter {
|
|||
),
|
||||
arguments,
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
envs,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
|
@ -245,7 +254,7 @@ impl DebugAdapter for JsDebugAdapter {
|
|||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge", "node-terminal"],
|
||||
"description": "The type of debug session",
|
||||
"default": "pwa-node"
|
||||
},
|
||||
|
@ -282,6 +291,10 @@ impl DebugAdapter for JsDebugAdapter {
|
|||
"description": "Automatically stop program after launch",
|
||||
"default": false
|
||||
},
|
||||
"attachSimplePort": {
|
||||
"type": "number",
|
||||
"description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
|
||||
},
|
||||
"runtimeExecutable": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
|
||||
|
@ -375,10 +388,6 @@ impl DebugAdapter for JsDebugAdapter {
|
|||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["program"] },
|
||||
{ "required": ["url"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -518,7 +527,11 @@ impl DebugAdapter for JsDebugAdapter {
|
|||
}
|
||||
|
||||
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
let label = args.configuration.get("name")?.as_str()?;
|
||||
let label = args
|
||||
.configuration
|
||||
.get("name")?
|
||||
.as_str()
|
||||
.filter(|name| !name.is_empty())?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -660,6 +660,15 @@ impl DebugAdapter for PythonDebugAdapter {
|
|||
self.get_installed_binary(delegate, &config, None, user_args, toolchain, false)
|
||||
.await
|
||||
}
|
||||
|
||||
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
let label = args
|
||||
.configuration
|
||||
.get("name")?
|
||||
.as_str()
|
||||
.filter(|label| !label.is_empty())?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version_from_github(
|
||||
|
|
|
@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter {
|
|||
.flatten()
|
||||
}
|
||||
|
||||
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
Err(anyhow::anyhow!("Not implemented"))
|
||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
self.extension
|
||||
.run_dap_locator(self.locator_name.as_ref().to_owned(), build_config)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -434,9 +434,14 @@ impl LogStore {
|
|||
|
||||
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
|
||||
self.projects.values_mut().for_each(|project| {
|
||||
project
|
||||
.debug_sessions
|
||||
.retain(|_, session| !session.is_terminated);
|
||||
let mut allowed_terminated_sessions = 10u32;
|
||||
project.debug_sessions.retain(|_, session| {
|
||||
if !session.is_terminated {
|
||||
return true;
|
||||
}
|
||||
allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
|
||||
allowed_terminated_sessions > 0
|
||||
});
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
@ -913,7 +918,13 @@ impl Render for DapLogView {
|
|||
}
|
||||
}
|
||||
|
||||
actions!(dev, [OpenDebugAdapterLogs]);
|
||||
actions!(
|
||||
dev,
|
||||
[
|
||||
/// Opens the debug adapter protocol logs viewer.
|
||||
OpenDebugAdapterLogs
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let log_store = cx.new(|cx| LogStore::new(cx));
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::session::running::breakpoint_list::BreakpointList;
|
|||
use crate::{
|
||||
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
|
||||
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
|
||||
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
|
||||
NewProcessModal, NewProcessMode, Pause, RerunSession, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
@ -25,7 +25,7 @@ use gpui::{
|
|||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Fs, ProjectPath, WorktreeId};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
|
@ -197,6 +197,7 @@ impl DebugPanel {
|
|||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map(|f| f.worktree_id(cx))
|
||||
});
|
||||
|
||||
let Some(worktree) = worktree
|
||||
.and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
|
||||
.or_else(|| self.project.read(cx).visible_worktrees(cx).next())
|
||||
|
@ -204,6 +205,7 @@ impl DebugPanel {
|
|||
log::debug!("Could not find a worktree to spawn the debug session in");
|
||||
return;
|
||||
};
|
||||
|
||||
self.debug_scenario_scheduled_last = true;
|
||||
if let Some(inventory) = self
|
||||
.project
|
||||
|
@ -214,7 +216,15 @@ impl DebugPanel {
|
|||
.cloned()
|
||||
{
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory.scenario_scheduled(scenario.clone());
|
||||
inventory.scenario_scheduled(
|
||||
scenario.clone(),
|
||||
// todo(debugger): Task context is cloned three times
|
||||
// once in Session,inventory, and in resolve scenario
|
||||
// we should wrap it in an RC instead to save some memory
|
||||
task_context.clone(),
|
||||
worktree_id,
|
||||
active_buffer.as_ref().map(|buffer| buffer.downgrade()),
|
||||
);
|
||||
})
|
||||
}
|
||||
let task = cx.spawn_in(window, {
|
||||
|
@ -225,6 +235,16 @@ impl DebugPanel {
|
|||
let definition = debug_session
|
||||
.update_in(cx, |debug_session, window, cx| {
|
||||
debug_session.running_state().update(cx, |running, cx| {
|
||||
if scenario.build.is_some() {
|
||||
running.scenario = Some(scenario.clone());
|
||||
running.scenario_context = Some(DebugScenarioContext {
|
||||
active_buffer: active_buffer
|
||||
.as_ref()
|
||||
.map(|entity| entity.downgrade()),
|
||||
task_context: task_context.clone(),
|
||||
worktree_id: worktree_id,
|
||||
});
|
||||
};
|
||||
running.resolve_scenario(
|
||||
scenario,
|
||||
task_context,
|
||||
|
@ -273,7 +293,8 @@ impl DebugPanel {
|
|||
return;
|
||||
};
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
|
||||
let Some((scenario, context)) = task_inventory.read(cx).last_scheduled_scenario().cloned()
|
||||
else {
|
||||
window.defer(cx, move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
|
@ -284,28 +305,22 @@ impl DebugPanel {
|
|||
return;
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_contexts = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
worktree_id,
|
||||
active_buffer,
|
||||
} = context;
|
||||
|
||||
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
|
||||
let worktree_id = task_contexts.worktree();
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.start_session(
|
||||
scenario.clone(),
|
||||
task_context,
|
||||
None,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
self.start_session(
|
||||
scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn register_session(
|
||||
|
@ -758,16 +773,16 @@ impl DebugPanel {
|
|||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.restart_session(cx);
|
||||
|this, _, window, cx| {
|
||||
this.rerun_session(window, cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Restart",
|
||||
&Restart,
|
||||
"Rerun Session",
|
||||
&RerunSession,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
|
@ -1298,11 +1313,13 @@ impl Render for DebugPanel {
|
|||
}
|
||||
|
||||
v_flex()
|
||||
.when_else(
|
||||
self.position(window, cx) == DockPosition::Bottom,
|
||||
|this| this.max_h(self.size),
|
||||
|this| this.max_w(self.size),
|
||||
)
|
||||
.when(!self.is_zoomed, |this| {
|
||||
this.when_else(
|
||||
self.position(window, cx) == DockPosition::Bottom,
|
||||
|this| this.max_h(self.size),
|
||||
|this| this.max_w(self.size),
|
||||
)
|
||||
})
|
||||
.size_full()
|
||||
.key_context("DebugPanel")
|
||||
.child(h_flex().children(self.top_controls_strip(window, cx)))
|
||||
|
@ -1600,12 +1617,13 @@ impl workspace::DebuggerProvider for DebuggerProvider {
|
|||
definition: DebugScenario,
|
||||
context: TaskContext,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.0.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.start_session(definition, context, buffer, None, window, cx);
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
this.start_session(definition, context, buffer, worktree_id, window, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,34 +32,67 @@ pub mod tests;
|
|||
actions!(
|
||||
debugger,
|
||||
[
|
||||
/// Starts a new debugging session.
|
||||
Start,
|
||||
/// Continues execution until the next breakpoint.
|
||||
Continue,
|
||||
/// Detaches the debugger from the running process.
|
||||
Detach,
|
||||
/// Pauses the currently running program.
|
||||
Pause,
|
||||
/// Restarts the current debugging session.
|
||||
Restart,
|
||||
/// Reruns the current debugging session with the same configuration.
|
||||
RerunSession,
|
||||
/// Steps into the next function call.
|
||||
StepInto,
|
||||
/// Steps over the current line.
|
||||
StepOver,
|
||||
/// Steps out of the current function.
|
||||
StepOut,
|
||||
/// Steps back to the previous statement.
|
||||
StepBack,
|
||||
/// Stops the debugging session.
|
||||
Stop,
|
||||
/// Toggles whether to ignore all breakpoints.
|
||||
ToggleIgnoreBreakpoints,
|
||||
/// Clears all breakpoints in the project.
|
||||
ClearAllBreakpoints,
|
||||
/// Focuses on the debugger console panel.
|
||||
FocusConsole,
|
||||
/// Focuses on the variables panel.
|
||||
FocusVariables,
|
||||
/// Focuses on the breakpoint list panel.
|
||||
FocusBreakpointList,
|
||||
/// Focuses on the call stack frames panel.
|
||||
FocusFrames,
|
||||
/// Focuses on the loaded modules panel.
|
||||
FocusModules,
|
||||
/// Focuses on the loaded sources panel.
|
||||
FocusLoadedSources,
|
||||
/// Focuses on the terminal panel.
|
||||
FocusTerminal,
|
||||
/// Shows the stack trace for the current thread.
|
||||
ShowStackTrace,
|
||||
/// Toggles the thread picker dropdown.
|
||||
ToggleThreadPicker,
|
||||
/// Toggles the session picker dropdown.
|
||||
ToggleSessionPicker,
|
||||
RerunLastSession,
|
||||
/// Reruns the last debugging session.
|
||||
#[action(deprecated_aliases = ["debugger::RerunLastSession"])]
|
||||
Rerun,
|
||||
/// Toggles expansion of the selected item in the debugger UI.
|
||||
ToggleExpandItem,
|
||||
]
|
||||
);
|
||||
|
||||
actions!(dev, [CopyDebugAdapterArguments]);
|
||||
actions!(
|
||||
dev,
|
||||
[
|
||||
/// Copies debug adapter launch arguments to clipboard.
|
||||
CopyDebugAdapterArguments
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
DebuggerSettings::register(cx);
|
||||
|
@ -74,17 +107,15 @@ pub fn init(cx: &mut App) {
|
|||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
.register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
|
@ -210,6 +241,14 @@ pub fn init(cx: &mut App) {
|
|||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &RerunSession, window, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.rerun_session(window, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
|
|
|
@ -4,6 +4,7 @@ use collections::HashMap;
|
|||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
|
@ -12,6 +13,8 @@ use crate::{
|
|||
|
||||
impl DebugPanel {
|
||||
fn dropdown_label(label: impl Into<SharedString>) -> Label {
|
||||
const MAX_LABEL_CHARS: usize = 50;
|
||||
let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS);
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}
|
||||
|
||||
|
@ -170,6 +173,8 @@ impl DebugPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<DropdownMenu> {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let running_state = running_state.clone();
|
||||
let running_state_read = running_state.read(cx);
|
||||
let thread_id = running_state_read.thread_id();
|
||||
|
@ -202,6 +207,7 @@ impl DebugPanel {
|
|||
.is_empty()
|
||||
.then(|| format!("Tid: {}", thread.id))
|
||||
.unwrap_or_else(|| thread.name);
|
||||
let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
|
||||
|
||||
this = this.entry(entry_name, None, move |window, cx| {
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
|
|
|
@ -23,7 +23,9 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use project::{
|
||||
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
|
||||
};
|
||||
use settings::{Settings, initial_local_debug_tasks_content};
|
||||
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
|
@ -92,6 +94,7 @@ impl NewProcessModal {
|
|||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
|
||||
// todo(debugger): get the buffer here (if the active item is an editor) and store it so we can pass it to start_session later
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?;
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
|
@ -1110,7 +1113,11 @@ pub(super) struct TaskMode {
|
|||
|
||||
pub(super) struct DebugDelegate {
|
||||
task_store: Entity<TaskStore>,
|
||||
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
|
||||
candidates: Vec<(
|
||||
Option<TaskSourceKind>,
|
||||
DebugScenario,
|
||||
Option<DebugScenarioContext>,
|
||||
)>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
prompt: String,
|
||||
|
@ -1208,7 +1215,11 @@ impl DebugDelegate {
|
|||
|
||||
this.delegate.candidates = recent
|
||||
.into_iter()
|
||||
.map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
|
||||
.map(|(scenario, context)| {
|
||||
let (kind, scenario) =
|
||||
Self::get_scenario_kind(&languages, &dap_registry, scenario);
|
||||
(kind, scenario, Some(context))
|
||||
})
|
||||
.chain(
|
||||
scenarios
|
||||
.into_iter()
|
||||
|
@ -1223,7 +1234,7 @@ impl DebugDelegate {
|
|||
.map(|(kind, scenario)| {
|
||||
let (language, scenario) =
|
||||
Self::get_scenario_kind(&languages, &dap_registry, scenario);
|
||||
(language.or(Some(kind)), scenario)
|
||||
(language.or(Some(kind)), scenario, None)
|
||||
}),
|
||||
)
|
||||
.collect();
|
||||
|
@ -1269,7 +1280,7 @@ impl PickerDelegate for DebugDelegate {
|
|||
let candidates: Vec<_> = candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (_, candidate))| {
|
||||
.map(|(index, (_, candidate, _))| {
|
||||
StringMatchCandidate::new(index, candidate.label.as_ref())
|
||||
})
|
||||
.collect();
|
||||
|
@ -1434,25 +1445,40 @@ impl PickerDelegate for DebugDelegate {
|
|||
.get(self.selected_index())
|
||||
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
|
||||
|
||||
let Some((_, debug_scenario)) = debug_scenario else {
|
||||
let Some((_, debug_scenario, context)) = debug_scenario else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some((
|
||||
task_contexts.active_context().cloned()?,
|
||||
task_contexts.worktree(),
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let context = context.unwrap_or_else(|| {
|
||||
self.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some(DebugScenarioContext {
|
||||
task_context: task_contexts.active_context().cloned()?,
|
||||
active_buffer: None,
|
||||
worktree_id: task_contexts.worktree(),
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use rpc::proto;
|
|||
use running::RunningState;
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
use ui::{Indicator, Tooltip, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
|
@ -126,7 +127,10 @@ impl DebugSession {
|
|||
}
|
||||
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let label = self.label(cx);
|
||||
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
|
||||
|
||||
let is_terminated = self
|
||||
.running_state
|
||||
|
|
|
@ -33,7 +33,7 @@ use language::Buffer;
|
|||
use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
use project::{
|
||||
Project, WorktreeId,
|
||||
DebugScenarioContext, Project, WorktreeId,
|
||||
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
|
@ -79,6 +79,8 @@ pub struct RunningState {
|
|||
pane_close_subscriptions: HashMap<EntityId, Subscription>,
|
||||
dock_axis: Axis,
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
pub(crate) scenario: Option<DebugScenario>,
|
||||
pub(crate) scenario_context: Option<DebugScenarioContext>,
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
|
@ -831,6 +833,8 @@ impl RunningState {
|
|||
debug_terminal,
|
||||
dock_axis,
|
||||
_schedule_serialize: None,
|
||||
scenario: None,
|
||||
scenario_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -900,7 +904,7 @@ impl RunningState {
|
|||
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
let mut extra_config = Value::Null;
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task_template, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
|
@ -930,6 +934,7 @@ impl RunningState {
|
|||
};
|
||||
|
||||
let locator_name = if let Some(locator_name) = locator_name {
|
||||
extra_config = config.clone();
|
||||
debug_assert!(!config_is_valid);
|
||||
Some(locator_name)
|
||||
} else if !config_is_valid {
|
||||
|
@ -945,6 +950,7 @@ impl RunningState {
|
|||
});
|
||||
if let Ok(t) = task {
|
||||
t.await.and_then(|scenario| {
|
||||
extra_config = scenario.config;
|
||||
match scenario.build {
|
||||
Some(BuildTaskDefinition::Template {
|
||||
locator_name, ..
|
||||
|
@ -967,7 +973,7 @@ impl RunningState {
|
|||
|
||||
let task_with_shell = SpawnInTerminal {
|
||||
command_label,
|
||||
command,
|
||||
command: Some(command),
|
||||
args,
|
||||
..task.resolved.clone()
|
||||
};
|
||||
|
@ -1008,13 +1014,13 @@ impl RunningState {
|
|||
if !exit_status.success() {
|
||||
anyhow::bail!("Build failed");
|
||||
}
|
||||
Some((task.resolved.clone(), locator_name))
|
||||
Some((task.resolved.clone(), locator_name, extra_config))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
} else if let Some((task, locator_name, extra_config)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.with_context(|| {
|
||||
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
|
||||
|
@ -1037,8 +1043,10 @@ impl RunningState {
|
|||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
|
||||
.await?;
|
||||
.await?;
|
||||
config = scenario.config;
|
||||
util::merge_non_null_json_value_into(extra_config, &mut config);
|
||||
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
} else {
|
||||
let Err(e) = request_type else {
|
||||
|
@ -1077,19 +1085,6 @@ impl RunningState {
|
|||
.map(PathBuf::from)
|
||||
.or_else(|| session.binary().unwrap().cwd.clone());
|
||||
|
||||
let mut args = request.args.clone();
|
||||
|
||||
// Handle special case for NodeJS debug adapter
|
||||
// If only the Node binary path is provided, we set the command to None
|
||||
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
|
||||
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
|
||||
// This allows the NodeJS debug client to attach correctly
|
||||
let command = if args.len() > 1 {
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut envs: HashMap<String, String> =
|
||||
self.session.read(cx).task_context().project_env.clone();
|
||||
if let Some(Value::Object(env)) = &request.env {
|
||||
|
@ -1103,32 +1098,58 @@ impl RunningState {
|
|||
}
|
||||
}
|
||||
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let kind = if let Some(command) = command {
|
||||
let title = request.title.clone().unwrap_or(command.clone());
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: task::TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: task::RevealStrategy::NoFocus,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
})
|
||||
let mut args = request.args.clone();
|
||||
let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
|
||||
// Handle special case for NodeJS debug adapter
|
||||
// If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
|
||||
// we set the command to None
|
||||
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
|
||||
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
|
||||
// This allows the NodeJS debug client to attach correctly
|
||||
if args
|
||||
.iter()
|
||||
.filter(|arg| !arg.starts_with("--"))
|
||||
.collect::<Vec<_>>()
|
||||
.len()
|
||||
> 1
|
||||
{
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if args.len() > 0 {
|
||||
Some(args.remove(0))
|
||||
} else {
|
||||
TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
|
||||
None
|
||||
};
|
||||
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
let title = request
|
||||
.title
|
||||
.clone()
|
||||
.filter(|title| !title.is_empty())
|
||||
.or_else(|| command.clone())
|
||||
.unwrap_or_else(|| "Debug terminal".to_string());
|
||||
let kind = TerminalKind::Task(task::SpawnInTerminal {
|
||||
id: task::TaskId("debug".to_string()),
|
||||
full_label: title.clone(),
|
||||
label: title.clone(),
|
||||
command: command.clone(),
|
||||
args,
|
||||
command_label: title.clone(),
|
||||
cwd,
|
||||
env: envs,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
reveal: task::RevealStrategy::NoFocus,
|
||||
reveal_target: task::RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell,
|
||||
show_summary: false,
|
||||
show_command: false,
|
||||
show_rerun: false,
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let weak_project = project.downgrade();
|
||||
|
||||
|
@ -1521,6 +1542,34 @@ impl RunningState {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn rerun_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some((scenario, context)) = self.scenario.take().zip(self.scenario_context.take())
|
||||
&& scenario.build.is_some()
|
||||
{
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.start_debug_session(
|
||||
scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
self.restart_session(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restart_session(&self, cx: &mut Context<Self>) {
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.restart(None, cx);
|
||||
|
|
|
@ -5,7 +5,8 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter};
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
|
@ -16,6 +17,7 @@ use project::{
|
|||
Project,
|
||||
debugger::{
|
||||
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
|
||||
dap_store::{DapStore, PersistedAdapterOptions},
|
||||
session::Session,
|
||||
},
|
||||
worktree_store::WorktreeStore,
|
||||
|
@ -33,7 +35,12 @@ use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
|
|||
|
||||
actions!(
|
||||
debugger,
|
||||
[PreviousBreakpointProperty, NextBreakpointProperty]
|
||||
[
|
||||
/// Navigates to the previous breakpoint property in the list.
|
||||
PreviousBreakpointProperty,
|
||||
/// Navigates to the next breakpoint property in the list.
|
||||
NextBreakpointProperty
|
||||
]
|
||||
);
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum SelectedBreakpointKind {
|
||||
|
@ -43,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind {
|
|||
pub(crate) struct BreakpointList {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
dap_store: Entity<DapStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
breakpoints: Vec<BreakpointEntry>,
|
||||
|
@ -54,6 +62,7 @@ pub(crate) struct BreakpointList {
|
|||
selected_ix: Option<usize>,
|
||||
input: Entity<Editor>,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointList {
|
||||
|
@ -80,24 +89,34 @@ impl BreakpointList {
|
|||
let project = project.read(cx);
|
||||
let breakpoint_store = project.breakpoint_store();
|
||||
let worktree_store = project.worktree_store();
|
||||
let dap_store = project.dap_store();
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
cx.new(|cx| Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
|
||||
cx.new(|cx| {
|
||||
let this = Self {
|
||||
breakpoint_store,
|
||||
dap_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
serialize_exception_breakpoints_task: None,
|
||||
};
|
||||
if let Some(name) = adapter_name {
|
||||
_ = this.deserialize_exception_breakpoints(name, cx);
|
||||
}
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -399,12 +418,8 @@ impl BreakpointList {
|
|||
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
if let Some(session) = &self.session {
|
||||
let id = exception_breakpoint.id.clone();
|
||||
session.update(cx, |session, cx| {
|
||||
session.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
}
|
||||
let id = exception_breakpoint.id.clone();
|
||||
self.toggle_exception_breakpoint(&id, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
|
@ -475,6 +490,64 @@ impl BreakpointList {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
if let Some(session) = &self.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(EXCEPTION_SERIALIZATION_INTERVAL)
|
||||
.await;
|
||||
this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
|
||||
.await?;
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn kvp_key(adapter_name: &str) -> String {
|
||||
format!("debug_adapter_`{adapter_name}`_persistence")
|
||||
}
|
||||
fn serialize_exception_breakpoints(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
if let Some(session) = self.session.as_ref() {
|
||||
let key = {
|
||||
let session = session.read(cx);
|
||||
let name = session.adapter().0;
|
||||
Self::kvp_key(&name)
|
||||
};
|
||||
let settings = self.dap_store.update(cx, |this, cx| {
|
||||
this.sync_adapter_options(session, cx);
|
||||
});
|
||||
let value = serde_json::to_string(&settings);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
|
||||
} else {
|
||||
return Task::ready(Result::Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_exception_breakpoints(
|
||||
&self,
|
||||
adapter_name: DebugAdapterName,
|
||||
cx: &mut Context<Self>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
|
||||
return Ok(());
|
||||
};
|
||||
let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
|
||||
self.dap_store
|
||||
.update(cx, |this, _| this.set_adapter_options(adapter_name, value));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
|
@ -983,12 +1056,7 @@ impl ExceptionBreakpoint {
|
|||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
if let Some(session) = &this.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use super::{
|
|||
use alacritty_terminal::vte::ansi;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use dap::{CompletionItem, CompletionItemType, OutputEvent};
|
||||
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
|
@ -13,17 +13,27 @@ use gpui::{
|
|||
Render, Subscription, Task, TextStyle, WeakEntity, actions,
|
||||
};
|
||||
use language::{Buffer, CodeLabel, ToOffset};
|
||||
use menu::Confirm;
|
||||
use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Completion, CompletionResponse,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
lsp_store::CompletionDocumentation,
|
||||
search_history::{SearchHistory, SearchHistoryCursor},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::fmt::Write;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
actions!(console, [WatchExpression]);
|
||||
actions!(
|
||||
console,
|
||||
[
|
||||
/// Adds an expression to the watch list.
|
||||
WatchExpression
|
||||
]
|
||||
);
|
||||
|
||||
pub struct Console {
|
||||
console: Entity<Editor>,
|
||||
|
@ -33,8 +43,10 @@ pub struct Console {
|
|||
variable_list: Entity<VariableList>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
last_token: OutputToken,
|
||||
update_output_task: Task<()>,
|
||||
update_output_task: Option<Task<()>>,
|
||||
focus_handle: FocusHandle,
|
||||
history: SearchHistory,
|
||||
cursor: SearchHistoryCursor,
|
||||
}
|
||||
|
||||
impl Console {
|
||||
|
@ -83,11 +95,6 @@ impl Console {
|
|||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
|
||||
if let SessionEvent::ConsoleOutput = event {
|
||||
this.update_output(window, cx)
|
||||
}
|
||||
}),
|
||||
cx.on_focus(&focus_handle, window, |console, window, cx| {
|
||||
if console.is_running(cx) {
|
||||
console.query_bar.focus_handle(cx).focus(window);
|
||||
|
@ -102,9 +109,14 @@ impl Console {
|
|||
variable_list,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
update_output_task: Task::ready(()),
|
||||
update_output_task: None,
|
||||
last_token: OutputToken(0),
|
||||
focus_handle,
|
||||
history: SearchHistory::new(
|
||||
None,
|
||||
project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
|
||||
),
|
||||
cursor: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,202 +145,116 @@ impl Console {
|
|||
self.session.read(cx).has_new_output(self.last_token)
|
||||
}
|
||||
|
||||
pub fn add_messages<'a>(
|
||||
fn add_messages(
|
||||
&mut self,
|
||||
events: impl Iterator<Item = &'a OutputEvent>,
|
||||
events: Vec<OutputEvent>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.console.update(cx, |console, cx| {
|
||||
console.set_read_only(false);
|
||||
) -> Task<Result<()>> {
|
||||
self.console.update(cx, |_, cx| {
|
||||
cx.spawn_in(window, async move |console, cx| {
|
||||
let mut len = console.update(cx, |this, cx| this.buffer().read(cx).len(cx))?;
|
||||
let (output, spans, background_spans) = cx
|
||||
.background_spawn(async move {
|
||||
let mut all_spans = Vec::new();
|
||||
let mut all_background_spans = Vec::new();
|
||||
let mut to_insert = String::new();
|
||||
let mut scratch = String::new();
|
||||
|
||||
for event in events {
|
||||
let to_insert = format!("{}\n", event.output.trim_end());
|
||||
for event in &events {
|
||||
scratch.clear();
|
||||
let mut ansi_handler = ConsoleHandler::default();
|
||||
let mut ansi_processor =
|
||||
ansi::Processor::<ansi::StdSyncHandler>::default();
|
||||
|
||||
let mut ansi_handler = ConsoleHandler::default();
|
||||
let mut ansi_processor = ansi::Processor::<ansi::StdSyncHandler>::default();
|
||||
let trimmed_output = event.output.trim_end();
|
||||
let _ = writeln!(&mut scratch, "{trimmed_output}");
|
||||
ansi_processor.advance(&mut ansi_handler, scratch.as_bytes());
|
||||
let output = std::mem::take(&mut ansi_handler.output);
|
||||
to_insert.extend(output.chars());
|
||||
let mut spans = std::mem::take(&mut ansi_handler.spans);
|
||||
let mut background_spans =
|
||||
std::mem::take(&mut ansi_handler.background_spans);
|
||||
if ansi_handler.current_range_start < output.len() {
|
||||
spans.push((
|
||||
ansi_handler.current_range_start..output.len(),
|
||||
ansi_handler.current_color,
|
||||
));
|
||||
}
|
||||
if ansi_handler.current_background_range_start < output.len() {
|
||||
background_spans.push((
|
||||
ansi_handler.current_background_range_start..output.len(),
|
||||
ansi_handler.current_background_color,
|
||||
));
|
||||
}
|
||||
|
||||
let len = console.buffer().read(cx).len(cx);
|
||||
ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes());
|
||||
let output = std::mem::take(&mut ansi_handler.output);
|
||||
let mut spans = std::mem::take(&mut ansi_handler.spans);
|
||||
let mut background_spans = std::mem::take(&mut ansi_handler.background_spans);
|
||||
if ansi_handler.current_range_start < output.len() {
|
||||
spans.push((
|
||||
ansi_handler.current_range_start..output.len(),
|
||||
ansi_handler.current_color,
|
||||
));
|
||||
}
|
||||
if ansi_handler.current_background_range_start < output.len() {
|
||||
background_spans.push((
|
||||
ansi_handler.current_background_range_start..output.len(),
|
||||
ansi_handler.current_background_color,
|
||||
));
|
||||
}
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(&output, window, cx);
|
||||
let buffer = console.buffer().read(cx).snapshot(cx);
|
||||
for (range, _) in spans.iter_mut() {
|
||||
let start_offset = len + range.start;
|
||||
*range = start_offset..len + range.end;
|
||||
}
|
||||
|
||||
struct ConsoleAnsiHighlight;
|
||||
for (range, _) in background_spans.iter_mut() {
|
||||
let start_offset = len + range.start;
|
||||
*range = start_offset..len + range.end;
|
||||
}
|
||||
|
||||
for (range, color) in spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = len + range.start;
|
||||
let range = start_offset..len + range.end;
|
||||
let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
let style = HighlightStyle {
|
||||
color: Some(terminal_view::terminal_element::convert_color(
|
||||
&color,
|
||||
cx.theme(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
console.highlight_text_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
vec![range],
|
||||
style,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
len += output.len();
|
||||
|
||||
for (range, color) in background_spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = len + range.start;
|
||||
let range = start_offset..len + range.end;
|
||||
let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
|
||||
let color_fetcher: fn(&Theme) -> Hsla = match color {
|
||||
// Named and theme defined colors
|
||||
ansi::Color::Named(n) => match n {
|
||||
ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
|
||||
ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
|
||||
ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
|
||||
ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
|
||||
ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
|
||||
ansi::NamedColor::Magenta => {
|
||||
|theme| theme.colors().terminal_ansi_magenta
|
||||
}
|
||||
ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
|
||||
ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
|
||||
ansi::NamedColor::BrightBlack => {
|
||||
|theme| theme.colors().terminal_ansi_bright_black
|
||||
}
|
||||
ansi::NamedColor::BrightRed => {
|
||||
|theme| theme.colors().terminal_ansi_bright_red
|
||||
}
|
||||
ansi::NamedColor::BrightGreen => {
|
||||
|theme| theme.colors().terminal_ansi_bright_green
|
||||
}
|
||||
ansi::NamedColor::BrightYellow => {
|
||||
|theme| theme.colors().terminal_ansi_bright_yellow
|
||||
}
|
||||
ansi::NamedColor::BrightBlue => {
|
||||
|theme| theme.colors().terminal_ansi_bright_blue
|
||||
}
|
||||
ansi::NamedColor::BrightMagenta => {
|
||||
|theme| theme.colors().terminal_ansi_bright_magenta
|
||||
}
|
||||
ansi::NamedColor::BrightCyan => {
|
||||
|theme| theme.colors().terminal_ansi_bright_cyan
|
||||
}
|
||||
ansi::NamedColor::BrightWhite => {
|
||||
|theme| theme.colors().terminal_ansi_bright_white
|
||||
}
|
||||
ansi::NamedColor::Foreground => {
|
||||
|theme| theme.colors().terminal_foreground
|
||||
}
|
||||
ansi::NamedColor::Background => {
|
||||
|theme| theme.colors().terminal_background
|
||||
}
|
||||
ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
|
||||
ansi::NamedColor::DimBlack => {
|
||||
|theme| theme.colors().terminal_ansi_dim_black
|
||||
}
|
||||
ansi::NamedColor::DimRed => {
|
||||
|theme| theme.colors().terminal_ansi_dim_red
|
||||
}
|
||||
ansi::NamedColor::DimGreen => {
|
||||
|theme| theme.colors().terminal_ansi_dim_green
|
||||
}
|
||||
ansi::NamedColor::DimYellow => {
|
||||
|theme| theme.colors().terminal_ansi_dim_yellow
|
||||
}
|
||||
ansi::NamedColor::DimBlue => {
|
||||
|theme| theme.colors().terminal_ansi_dim_blue
|
||||
}
|
||||
ansi::NamedColor::DimMagenta => {
|
||||
|theme| theme.colors().terminal_ansi_dim_magenta
|
||||
}
|
||||
ansi::NamedColor::DimCyan => {
|
||||
|theme| theme.colors().terminal_ansi_dim_cyan
|
||||
}
|
||||
ansi::NamedColor::DimWhite => {
|
||||
|theme| theme.colors().terminal_ansi_dim_white
|
||||
}
|
||||
ansi::NamedColor::BrightForeground => {
|
||||
|theme| theme.colors().terminal_bright_foreground
|
||||
}
|
||||
ansi::NamedColor::DimForeground => {
|
||||
|theme| theme.colors().terminal_dim_foreground
|
||||
}
|
||||
},
|
||||
// 'True' colors
|
||||
ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
|
||||
// 8 bit, indexed colors
|
||||
ansi::Color::Indexed(i) => {
|
||||
match i {
|
||||
// 0-15 are the same as the named colors above
|
||||
0 => |theme| theme.colors().terminal_ansi_black,
|
||||
1 => |theme| theme.colors().terminal_ansi_red,
|
||||
2 => |theme| theme.colors().terminal_ansi_green,
|
||||
3 => |theme| theme.colors().terminal_ansi_yellow,
|
||||
4 => |theme| theme.colors().terminal_ansi_blue,
|
||||
5 => |theme| theme.colors().terminal_ansi_magenta,
|
||||
6 => |theme| theme.colors().terminal_ansi_cyan,
|
||||
7 => |theme| theme.colors().terminal_ansi_white,
|
||||
8 => |theme| theme.colors().terminal_ansi_bright_black,
|
||||
9 => |theme| theme.colors().terminal_ansi_bright_red,
|
||||
10 => |theme| theme.colors().terminal_ansi_bright_green,
|
||||
11 => |theme| theme.colors().terminal_ansi_bright_yellow,
|
||||
12 => |theme| theme.colors().terminal_ansi_bright_blue,
|
||||
13 => |theme| theme.colors().terminal_ansi_bright_magenta,
|
||||
14 => |theme| theme.colors().terminal_ansi_bright_cyan,
|
||||
15 => |theme| theme.colors().terminal_ansi_bright_white,
|
||||
// 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
|
||||
// See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
|
||||
// 16..=231 => {
|
||||
// let (r, g, b) = rgb_for_index(index as u8);
|
||||
// rgba_color(
|
||||
// if r == 0 { 0 } else { r * 40 + 55 },
|
||||
// if g == 0 { 0 } else { g * 40 + 55 },
|
||||
// if b == 0 { 0 } else { b * 40 + 55 },
|
||||
// )
|
||||
// }
|
||||
// 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
|
||||
// 232..=255 => {
|
||||
// let i = index as u8 - 232; // Align index to 0..24
|
||||
// let value = i * 10 + 8;
|
||||
// rgba_color(value, value, value)
|
||||
// }
|
||||
// For compatibility with the alacritty::Colors interface
|
||||
// See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
|
||||
_ => |_| gpui::black(),
|
||||
}
|
||||
all_spans.extend(spans);
|
||||
all_background_spans.extend(background_spans);
|
||||
}
|
||||
};
|
||||
(to_insert, all_spans, all_background_spans)
|
||||
})
|
||||
.await;
|
||||
console.update_in(cx, |console, window, cx| {
|
||||
console.set_read_only(false);
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(&output, window, cx);
|
||||
console.set_read_only(true);
|
||||
|
||||
console.highlight_background_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
&[range],
|
||||
color_fetcher,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
struct ConsoleAnsiHighlight;
|
||||
|
||||
console.set_read_only(true);
|
||||
cx.notify();
|
||||
});
|
||||
let buffer = console.buffer().read(cx).snapshot(cx);
|
||||
|
||||
for (range, color) in spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = range.start;
|
||||
let range =
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
let style = HighlightStyle {
|
||||
color: Some(terminal_view::terminal_element::convert_color(
|
||||
&color,
|
||||
cx.theme(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
console.highlight_text_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
vec![range],
|
||||
style,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
for (range, color) in background_spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = range.start;
|
||||
let range =
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
console.highlight_background_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
&[range],
|
||||
color_fetcher(color),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn watch_expression(
|
||||
|
@ -345,7 +271,8 @@ impl Console {
|
|||
|
||||
expression
|
||||
});
|
||||
|
||||
self.history.add(&mut self.cursor, expression.clone());
|
||||
self.cursor.reset();
|
||||
self.session.update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(
|
||||
|
@ -365,7 +292,28 @@ impl Console {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let prev = self.history.previous(&mut self.cursor);
|
||||
if let Some(prev) = prev {
|
||||
self.query_bar.update(cx, |editor, cx| {
|
||||
editor.set_text(prev, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn next_query(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let next = self.history.next(&mut self.cursor);
|
||||
let query = next.unwrap_or_else(|| {
|
||||
self.cursor.reset();
|
||||
""
|
||||
});
|
||||
|
||||
self.query_bar.update(cx, |editor, cx| {
|
||||
editor.set_text(query, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let expression = self.query_bar.update(cx, |editor, cx| {
|
||||
let expression = editor.text(cx);
|
||||
cx.defer_in(window, |editor, window, cx| {
|
||||
|
@ -375,6 +323,8 @@ impl Console {
|
|||
expression
|
||||
});
|
||||
|
||||
self.history.add(&mut self.cursor, expression.clone());
|
||||
self.cursor.reset();
|
||||
self.session.update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(
|
||||
|
@ -458,31 +408,50 @@ impl Console {
|
|||
EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
|
||||
}
|
||||
|
||||
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.update_output_task.is_some() {
|
||||
return;
|
||||
}
|
||||
let session = self.session.clone();
|
||||
let token = self.last_token;
|
||||
self.update_output_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let Some((last_processed_token, task)) = session
|
||||
.update_in(cx, |session, window, cx| {
|
||||
let (output, last_processed_token) = session.output(token);
|
||||
|
||||
self.update_output_task = cx.spawn_in(window, async move |this, cx| {
|
||||
_ = session.update_in(cx, move |session, window, cx| {
|
||||
let (output, last_processed_token) = session.output(token);
|
||||
|
||||
_ = this.update(cx, |this, cx| {
|
||||
if last_processed_token == this.last_token {
|
||||
return;
|
||||
}
|
||||
this.add_messages(output, window, cx);
|
||||
|
||||
this.last_token = last_processed_token;
|
||||
this.update(cx, |this, cx| {
|
||||
if last_processed_token == this.last_token {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
last_processed_token,
|
||||
this.add_messages(output.cloned().collect(), window, cx),
|
||||
))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
_ = this.update(cx, |this, _| {
|
||||
this.update_output_task.take();
|
||||
});
|
||||
return;
|
||||
};
|
||||
_ = task.await.log_err();
|
||||
_ = this.update(cx, |this, _| {
|
||||
this.last_token = last_processed_token;
|
||||
this.update_output_task.take();
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Console {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let query_focus_handle = self.query_bar.focus_handle(cx);
|
||||
|
||||
self.update_output(window, cx);
|
||||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("DebugConsole")
|
||||
|
@ -493,6 +462,8 @@ impl Render for Console {
|
|||
.when(self.is_running(cx), |this| {
|
||||
this.child(Divider::horizontal()).child(
|
||||
h_flex()
|
||||
.on_action(cx.listener(Self::previous_query))
|
||||
.on_action(cx.listener(Self::next_query))
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.render_query_bar(cx))
|
||||
|
@ -585,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
trigger_in_words: bool,
|
||||
menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let char = if let Some(char) = chars.next() {
|
||||
char
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
|
||||
return false;
|
||||
}
|
||||
|
||||
let classifier = snapshot.char_classifier_at(position).for_completion(true);
|
||||
if trigger_in_words && classifier.is_word(char) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.0
|
||||
.read_with(cx, |console, cx| {
|
||||
console
|
||||
|
@ -626,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
variable_list.completion_variables(cx)
|
||||
}) {
|
||||
if let Some(evaluate_name) = &variable.evaluate_name {
|
||||
variables.insert(evaluate_name.clone(), variable.value.clone());
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
if variables
|
||||
.insert(evaluate_name.clone(), variable.value.clone())
|
||||
.is_none()
|
||||
{
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
variables.insert(variable.name.clone(), variable.value.clone());
|
||||
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
if variables
|
||||
.insert(variable.name.clone(), variable.value.clone())
|
||||
.is_none()
|
||||
{
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(variables, string_matches)
|
||||
|
@ -686,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string, variable_value),
|
||||
text: string_match.string.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
|
||||
variable_value.into(),
|
||||
)),
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
|
@ -705,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
})
|
||||
}
|
||||
|
||||
const fn completion_type_score(completion_type: CompletionItemType) -> usize {
|
||||
match completion_type {
|
||||
CompletionItemType::Field | CompletionItemType::Property => 0,
|
||||
CompletionItemType::Variable | CompletionItemType::Value => 1,
|
||||
CompletionItemType::Method
|
||||
| CompletionItemType::Function
|
||||
| CompletionItemType::Constructor => 2,
|
||||
CompletionItemType::Class
|
||||
| CompletionItemType::Interface
|
||||
| CompletionItemType::Module => 3,
|
||||
_ => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
|
||||
completion_item.sort_text.clone().unwrap_or_else(|| {
|
||||
format!(
|
||||
"{:03}_{}",
|
||||
Self::completion_type_score(
|
||||
completion_item.type_.unwrap_or(CompletionItemType::Text)
|
||||
),
|
||||
completion_item.label.to_ascii_lowercase()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn client_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
|
@ -729,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
let completions = completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let sort_text = Self::completion_item_sort_text(&completion);
|
||||
let new_text = completion
|
||||
.text
|
||||
.as_ref()
|
||||
|
@ -761,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
documentation: completion.detail.map(|detail| {
|
||||
CompletionDocumentation::MultiLineMarkdown(detail.into())
|
||||
}),
|
||||
confirm: None,
|
||||
source: project::CompletionSource::BufferWord {
|
||||
word_range: buffer_position..language::Anchor::MAX,
|
||||
resolved: false,
|
||||
},
|
||||
source: project::CompletionSource::Dap { sort_text },
|
||||
insert_text_mode: None,
|
||||
}
|
||||
})
|
||||
|
@ -845,3 +863,84 @@ impl ansi::Handler for ConsoleHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla {
|
||||
let color_fetcher: fn(&Theme) -> Hsla = match color {
|
||||
// Named and theme defined colors
|
||||
ansi::Color::Named(n) => match n {
|
||||
ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
|
||||
ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
|
||||
ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
|
||||
ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
|
||||
ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
|
||||
ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta,
|
||||
ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
|
||||
ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
|
||||
ansi::NamedColor::BrightBlack => |theme| theme.colors().terminal_ansi_bright_black,
|
||||
ansi::NamedColor::BrightRed => |theme| theme.colors().terminal_ansi_bright_red,
|
||||
ansi::NamedColor::BrightGreen => |theme| theme.colors().terminal_ansi_bright_green,
|
||||
ansi::NamedColor::BrightYellow => |theme| theme.colors().terminal_ansi_bright_yellow,
|
||||
ansi::NamedColor::BrightBlue => |theme| theme.colors().terminal_ansi_bright_blue,
|
||||
ansi::NamedColor::BrightMagenta => |theme| theme.colors().terminal_ansi_bright_magenta,
|
||||
ansi::NamedColor::BrightCyan => |theme| theme.colors().terminal_ansi_bright_cyan,
|
||||
ansi::NamedColor::BrightWhite => |theme| theme.colors().terminal_ansi_bright_white,
|
||||
ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground,
|
||||
ansi::NamedColor::Background => |theme| theme.colors().terminal_background,
|
||||
ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
|
||||
ansi::NamedColor::DimBlack => |theme| theme.colors().terminal_ansi_dim_black,
|
||||
ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red,
|
||||
ansi::NamedColor::DimGreen => |theme| theme.colors().terminal_ansi_dim_green,
|
||||
ansi::NamedColor::DimYellow => |theme| theme.colors().terminal_ansi_dim_yellow,
|
||||
ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue,
|
||||
ansi::NamedColor::DimMagenta => |theme| theme.colors().terminal_ansi_dim_magenta,
|
||||
ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan,
|
||||
ansi::NamedColor::DimWhite => |theme| theme.colors().terminal_ansi_dim_white,
|
||||
ansi::NamedColor::BrightForeground => |theme| theme.colors().terminal_bright_foreground,
|
||||
ansi::NamedColor::DimForeground => |theme| theme.colors().terminal_dim_foreground,
|
||||
},
|
||||
// 'True' colors
|
||||
ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
|
||||
// 8 bit, indexed colors
|
||||
ansi::Color::Indexed(i) => {
|
||||
match i {
|
||||
// 0-15 are the same as the named colors above
|
||||
0 => |theme| theme.colors().terminal_ansi_black,
|
||||
1 => |theme| theme.colors().terminal_ansi_red,
|
||||
2 => |theme| theme.colors().terminal_ansi_green,
|
||||
3 => |theme| theme.colors().terminal_ansi_yellow,
|
||||
4 => |theme| theme.colors().terminal_ansi_blue,
|
||||
5 => |theme| theme.colors().terminal_ansi_magenta,
|
||||
6 => |theme| theme.colors().terminal_ansi_cyan,
|
||||
7 => |theme| theme.colors().terminal_ansi_white,
|
||||
8 => |theme| theme.colors().terminal_ansi_bright_black,
|
||||
9 => |theme| theme.colors().terminal_ansi_bright_red,
|
||||
10 => |theme| theme.colors().terminal_ansi_bright_green,
|
||||
11 => |theme| theme.colors().terminal_ansi_bright_yellow,
|
||||
12 => |theme| theme.colors().terminal_ansi_bright_blue,
|
||||
13 => |theme| theme.colors().terminal_ansi_bright_magenta,
|
||||
14 => |theme| theme.colors().terminal_ansi_bright_cyan,
|
||||
15 => |theme| theme.colors().terminal_ansi_bright_white,
|
||||
// 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
|
||||
// See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
|
||||
// 16..=231 => {
|
||||
// let (r, g, b) = rgb_for_index(index as u8);
|
||||
// rgba_color(
|
||||
// if r == 0 { 0 } else { r * 40 + 55 },
|
||||
// if g == 0 { 0 } else { g * 40 + 55 },
|
||||
// if b == 0 { 0 } else { b * 40 + 55 },
|
||||
// )
|
||||
// }
|
||||
// 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
|
||||
// 232..=255 => {
|
||||
// let i = index as u8 - 232; // Align index to 0..24
|
||||
// let value = i * 10 + 8;
|
||||
// rgba_color(value, value, value)
|
||||
// }
|
||||
// For compatibility with the alacritty::Colors interface
|
||||
// See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
|
||||
_ => |_| gpui::black(),
|
||||
}
|
||||
}
|
||||
};
|
||||
color_fetcher
|
||||
}
|
||||
|
|
|
@ -18,12 +18,19 @@ use util::debug_panic;
|
|||
actions!(
|
||||
variable_list,
|
||||
[
|
||||
/// Expands the selected variable entry to show its children.
|
||||
ExpandSelectedEntry,
|
||||
/// Collapses the selected variable entry to hide its children.
|
||||
CollapseSelectedEntry,
|
||||
/// Copies the variable name to the clipboard.
|
||||
CopyVariableName,
|
||||
/// Copies the variable value to the clipboard.
|
||||
CopyVariableValue,
|
||||
/// Edits the value of the selected variable.
|
||||
EditVariable,
|
||||
/// Adds the selected variable to the watch list.
|
||||
AddWatch,
|
||||
/// Removes the selected variable from the watch list.
|
||||
RemoveWatch,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -115,6 +115,7 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
|||
config.to_scenario(),
|
||||
TaskContext::default(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue