Compare commits
1 Commits
implement-
...
fix-python
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
514121d6d4 |
28
.github/workflows/run_agent_eval_daily.yml
vendored
28
.github/workflows/run_agent_eval_daily.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Run Eval Daily
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
run_eval:
|
||||
name: Run Eval
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo eval
|
||||
run: cargo run -p eval
|
||||
139
Cargo.lock
generated
139
Cargo.lock
generated
@@ -324,7 +324,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -337,9 +337,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -567,7 +567,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -704,7 +704,6 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
@@ -722,11 +721,9 @@ dependencies = [
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"web_search",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1884,7 +1881,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
@@ -3031,7 +3028,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
@@ -3051,7 +3048,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3364,7 +3360,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -4481,7 +4477,7 @@ dependencies = [
|
||||
"optfield",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
@@ -4890,7 +4886,6 @@ dependencies = [
|
||||
"collections",
|
||||
"context_server",
|
||||
"dap",
|
||||
"dirs 5.0.1",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"fs",
|
||||
@@ -4912,11 +4907,9 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"shellexpand 2.1.2",
|
||||
"telemetry",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -5126,7 +5119,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5977,7 +5970,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -6070,7 +6063,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -6176,7 +6169,7 @@ dependencies = [
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"thiserror 2.0.12",
|
||||
@@ -6824,7 +6817,7 @@ name = "icons"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -7092,16 +7085,16 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
@@ -7678,7 +7671,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
@@ -7738,7 +7731,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"tiktoken-rs",
|
||||
@@ -7746,7 +7739,6 @@ dependencies = [
|
||||
"ui",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7962,9 +7954,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8635,9 +8627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.45"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -8711,7 +8703,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -9558,7 +9550,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -12137,7 +12129,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"tracing",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -12665,7 +12657,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -13261,9 +13253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -13710,7 +13702,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"story",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
@@ -13792,16 +13784,7 @@ version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.1",
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13817,19 +13800,6 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -14445,7 +14415,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -14479,7 +14449,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"simplelog",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"vscode_theme",
|
||||
"workspace-hack",
|
||||
@@ -15480,7 +15450,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"ui_macros",
|
||||
"util",
|
||||
@@ -16613,36 +16583,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"gpui",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search_providers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language_model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.8"
|
||||
@@ -17681,7 +17621,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sqlez",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
@@ -17826,7 +17766,7 @@ dependencies = [
|
||||
"sqlx-macros-core",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"subtle",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.100",
|
||||
@@ -18198,7 +18138,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.184.0"
|
||||
version = "0.183.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18321,8 +18261,6 @@ dependencies = [
|
||||
"uuid",
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"welcome",
|
||||
"windows 0.61.1",
|
||||
"winresource",
|
||||
@@ -18387,13 +18325,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.5.1"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ee4d410dbc030c3e6e3af78fc76296f6bebe20dcb6d7d3fa24bca306fc8c1ce"
|
||||
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -165,8 +165,6 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -372,8 +370,6 @@ util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
web_search_providers = { path = "crates/web_search_providers" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -540,7 +536,7 @@ smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
strum = { version = "0.26.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
@@ -605,7 +601,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.5.1"
|
||||
zed_llm_client = "0.4"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
@@ -134,9 +134,7 @@
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -632,7 +630,6 @@
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
@@ -721,7 +718,7 @@
|
||||
"alt-shift-copy": "workspace::CopyRelativePath",
|
||||
"alt-ctrl-shift-c": "workspace::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
|
||||
@@ -286,7 +286,6 @@
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-p": "assistant::OpenPromptLibrary",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
@@ -542,9 +541,7 @@
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -789,7 +786,7 @@
|
||||
"cmd-alt-c": "workspace::CopyPath",
|
||||
"alt-cmd-shift-c": "workspace::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
|
||||
@@ -652,8 +652,7 @@
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
"thinking": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
@@ -679,8 +678,7 @@
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
"thinking": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
use crate::context::{AssistantContext, ContextId, format_context_as_string};
|
||||
use crate::context::{AssistantContext, ContextId};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
|
||||
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
|
||||
use gpui::{
|
||||
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment,
|
||||
ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
|
||||
MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, Role, StopReason,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role, StopReason};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
|
||||
use project::ProjectItem as _;
|
||||
@@ -684,9 +681,6 @@ fn open_markdown_link(
|
||||
|
||||
struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
_subscription: Subscription,
|
||||
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
@@ -786,13 +780,6 @@ impl ActiveThread {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
/// Returns the editing message id and the estimated token count in the content
|
||||
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
|
||||
self.editing_message
|
||||
.as_ref()
|
||||
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
|
||||
}
|
||||
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
@@ -956,8 +943,8 @@ impl ActiveThread {
|
||||
&tool_use.input,
|
||||
self.thread
|
||||
.read(cx)
|
||||
.output_for_tool(&tool_use.id)
|
||||
.map(|output| output.clone().into())
|
||||
.tool_result(&tool_use.id)
|
||||
.map(|result| result.content.clone().into())
|
||||
.unwrap_or("".into()),
|
||||
cx,
|
||||
);
|
||||
@@ -1138,91 +1125,15 @@ impl ActiveThread {
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor
|
||||
});
|
||||
let subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
this.update_editing_message_token_count(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
self.editing_message = Some((
|
||||
message_id,
|
||||
EditMessageState {
|
||||
editor: editor.clone(),
|
||||
last_estimated_token_count: None,
|
||||
_subscription: subscription,
|
||||
_update_token_count_task: None,
|
||||
},
|
||||
));
|
||||
self.update_editing_message_token_count(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
let Some((message_id, state)) = self.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
state._update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
state.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
|
||||
let editor = state.editor.clone();
|
||||
let thread = self.thread.clone();
|
||||
let message_id = *message_id;
|
||||
|
||||
state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let context = thread.read(cx).context_for_message(message_id);
|
||||
let new_context = thread.read(cx).filter_new_context(context);
|
||||
let context_text =
|
||||
format_context_as_string(new_context, cx).unwrap_or(String::new());
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
let content = context_text + &message_text;
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
cache: false,
|
||||
}],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editing_message.take();
|
||||
cx.notify();
|
||||
@@ -1764,9 +1675,6 @@ impl ActiveThread {
|
||||
"confirm-edit-message",
|
||||
"Regenerate",
|
||||
)
|
||||
.disabled(
|
||||
edit_message_editor.read(cx).is_empty(cx),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
@@ -1829,16 +1737,8 @@ impl ActiveThread {
|
||||
),
|
||||
};
|
||||
|
||||
let after_editing_message = self
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.map_or(false, |(editing_message_id, _)| {
|
||||
message_id > *editing_message_id
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when(after_editing_message, |parent| parent.opacity(0.2))
|
||||
.when_some(checkpoint, |parent, checkpoint| {
|
||||
let mut is_pending = false;
|
||||
let mut error = None;
|
||||
@@ -2379,15 +2279,12 @@ impl ActiveThread {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
return card.render(&tool_use.status, window, cx);
|
||||
}
|
||||
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
|
||||
|
||||
let fs = self
|
||||
@@ -2446,9 +2343,6 @@ impl ActiveThread {
|
||||
rendered.input.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
@@ -2475,16 +2369,12 @@ impl ActiveThread {
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
)),
|
||||
),
|
||||
@@ -2541,7 +2431,6 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
@@ -2655,7 +2544,7 @@ impl ActiveThread {
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.my_2()
|
||||
.my_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -2872,7 +2761,7 @@ impl ActiveThread {
|
||||
)
|
||||
})
|
||||
}
|
||||
}).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -3064,12 +2953,6 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveThreadEvent {
|
||||
EditingMessageTokenCountChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashSet;
|
||||
use editor::{
|
||||
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
@@ -355,24 +355,16 @@ impl AgentDiff {
|
||||
self.update_selection(&diff_hunks_in_ranges, window, cx);
|
||||
}
|
||||
|
||||
let mut ranges_by_buffer = HashMap::default();
|
||||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
ranges_by_buffer
|
||||
.entry(buffer.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(hunk.buffer_range.clone());
|
||||
self.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
|
||||
@@ -9,14 +9,11 @@ use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription,
|
||||
};
|
||||
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
|
||||
Switch, Tooltip, prelude::*,
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
@@ -34,8 +31,6 @@ pub struct AssistantConfiguration {
|
||||
expanded_context_server_tools: HashMap<Arc<str>, bool>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl AssistantConfiguration {
|
||||
@@ -65,9 +60,6 @@ impl AssistantConfiguration {
|
||||
},
|
||||
);
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
focus_handle,
|
||||
@@ -76,8 +68,6 @@ impl AssistantConfiguration {
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
this
|
||||
@@ -119,7 +109,7 @@ pub enum AssistantConfigurationEvent {
|
||||
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
|
||||
|
||||
impl AssistantConfiguration {
|
||||
fn render_provider_configuration_block(
|
||||
fn render_provider_configuration(
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -174,7 +164,7 @@ impl AssistantConfiguration {
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
@@ -185,33 +175,6 @@ impl AssistantConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_4()
|
||||
.flex_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_configuration_block(&provider, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
@@ -219,7 +182,6 @@ impl AssistantConfiguration {
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
@@ -271,7 +233,6 @@ impl AssistantConfiguration {
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
@@ -465,51 +426,39 @@ impl AssistantConfiguration {
|
||||
|
||||
impl Render for AssistantConfiguration {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
v_flex()
|
||||
.id("assistant-configuration")
|
||||
.key_context("AgentConfiguration")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.relative()
|
||||
.size_full()
|
||||
.pb_8()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(
|
||||
v_flex()
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("assistant-configuration-scrollbar")
|
||||
.occlude()
|
||||
.absolute()
|
||||
.right(px(3.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.pb_6()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.mt_1()
|
||||
.gap_6()
|
||||
.flex_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_configuration(&provider, cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
|
||||
humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
@@ -25,7 +25,6 @@ use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
use ui::{
|
||||
@@ -37,10 +36,10 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
|
||||
|
||||
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -181,8 +180,8 @@ pub struct AssistantPanel {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<ActiveThread>,
|
||||
_thread_subscription: Subscription,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_active_thread_subscriptions: Vec<Subscription>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
context_editor: Option<Entity<ContextEditor>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
@@ -264,13 +263,6 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let history_store =
|
||||
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
|
||||
|
||||
@@ -295,12 +287,6 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -309,12 +295,8 @@ impl AssistantPanel {
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
thread,
|
||||
_thread_subscription: thread_subscription,
|
||||
message_editor,
|
||||
_active_thread_subscriptions: vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
],
|
||||
context_store,
|
||||
context_editor: None,
|
||||
configuration: None,
|
||||
@@ -399,13 +381,6 @@ impl AssistantPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
@@ -418,12 +393,12 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
@@ -437,19 +412,6 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -575,13 +537,6 @@ impl AssistantPanel {
|
||||
Some(this.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
@@ -593,14 +548,6 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
this.fs.clone(),
|
||||
@@ -613,19 +560,6 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -918,7 +852,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
|
||||
fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
|
||||
|
||||
let content = match &self.active_view {
|
||||
@@ -978,8 +912,13 @@ impl AssistantPanel {
|
||||
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_thread = self.thread.read(cx);
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let token_usage = thread.total_token_usage(cx);
|
||||
let thread_id = thread.id().clone();
|
||||
|
||||
let is_generating = thread.is_generating();
|
||||
let is_empty = active_thread.is_empty();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let is_history = matches!(self.active_view, ActiveView::History);
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
@@ -988,8 +927,6 @@ impl AssistantPanel {
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let go_back_button = match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => Some(
|
||||
div().pl_1().child(
|
||||
@@ -1036,9 +973,69 @@ impl AssistantPanel {
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.when(show_token_count, |parent|
|
||||
parent.children(self.render_token_count(&thread, cx))
|
||||
)
|
||||
.when(show_token_count, |parent| match self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if token_usage.total == 0 {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let token_color = match token_usage.ratio {
|
||||
TokenUsageRatio::Normal => Color::Muted,
|
||||
TokenUsageRatio::Warning => Color::Warning,
|
||||
TokenUsageRatio::Exceeded => Color::Error,
|
||||
};
|
||||
|
||||
parent.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(assistant_context_editor::humanize_token_count(
|
||||
token_usage.total,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_color)
|
||||
.map(|label| {
|
||||
if is_generating {
|
||||
label
|
||||
.with_animation(
|
||||
"used-tokens-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(
|
||||
0.6, 1.,
|
||||
)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
label.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new("/").size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(assistant_context_editor::humanize_token_count(
|
||||
token_usage.max,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let Some(editor) = self.context_editor.as_ref() else {
|
||||
return parent;
|
||||
};
|
||||
let Some(element) = render_remaining_tokens(editor, cx) else {
|
||||
return parent;
|
||||
};
|
||||
parent.child(element)
|
||||
}
|
||||
_ => parent,
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
@@ -1115,16 +1112,16 @@ impl AssistantPanel {
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.action("Settings", OpenConfiguration.boxed_clone())
|
||||
.separator()
|
||||
.action(
|
||||
"Install MCPs",
|
||||
Box::new(zed_actions::Extensions {
|
||||
zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
))
|
||||
@@ -1134,111 +1131,6 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
|
||||
let is_generating = thread.is_generating();
|
||||
let message_editor = self.message_editor.read(cx);
|
||||
|
||||
let conversation_token_usage = thread.total_token_usage(cx);
|
||||
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
|
||||
self.thread.read(cx).editing_message_id()
|
||||
{
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id, cx)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
} else {
|
||||
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
|
||||
let combined = conversation_token_usage.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
};
|
||||
|
||||
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
|
||||
|
||||
match self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if total_token_usage.total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_color = match total_token_usage.ratio() {
|
||||
TokenUsageRatio::Normal if is_estimating => Color::Default,
|
||||
TokenUsageRatio::Normal => Color::Muted,
|
||||
TokenUsageRatio::Warning => Color::Warning,
|
||||
TokenUsageRatio::Exceeded => Color::Error,
|
||||
};
|
||||
|
||||
let token_count = h_flex()
|
||||
.id("token-count")
|
||||
.flex_shrink_0()
|
||||
.gap_0p5()
|
||||
.when(!is_generating && is_estimating, |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex()
|
||||
.mr_0p5()
|
||||
.size_2()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text.opacity(0.1))
|
||||
.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().colors().text),
|
||||
),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Estimated New Token Count",
|
||||
None,
|
||||
format!(
|
||||
"Current Conversation Tokens: {}",
|
||||
humanize_token_count(conversation_token_usage.total)
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(humanize_token_count(total_token_usage.total))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_color)
|
||||
.map(|label| {
|
||||
if is_generating || is_waiting_to_update_token_count {
|
||||
label
|
||||
.with_animation(
|
||||
"used-tokens-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
label.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(total_token_usage.max))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let editor = self.context_editor.as_ref()?;
|
||||
let element = render_remaining_tokens(editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1557,9 +1449,6 @@ impl AssistantPanel {
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
ThreadError::ModelRequestLimitReached { plan } => {
|
||||
self.render_model_request_limit_reached_error(plan, cx)
|
||||
}
|
||||
ThreadError::Message { header, message } => {
|
||||
self.render_error_message(header, message, cx)
|
||||
}
|
||||
@@ -1662,71 +1551,6 @@ impl AssistantPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
&self,
|
||||
plan: Plan,
|
||||
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",
|
||||
};
|
||||
|
||||
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()
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
&self,
|
||||
header: SharedString,
|
||||
@@ -1899,27 +1723,10 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
_workspace: &mut Workspace,
|
||||
_creases: Vec<(String, String)>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !panel.focus_handle(cx).contains_focused(window, cx) {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
cx.defer_in(window, move |panel, window, cx| {
|
||||
if let Some(context) = panel.active_context_editor() {
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::sync::atomic::AtomicBool;
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
@@ -38,24 +37,7 @@ pub(crate) enum Match {
|
||||
File(FileMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Mode(ModeMatch),
|
||||
}
|
||||
|
||||
pub struct ModeMatch {
|
||||
mat: Option<StringMatch>,
|
||||
mode: ContextPickerMode,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
pub fn score(&self) -> f64 {
|
||||
match self {
|
||||
Match::File(file) => file.mat.score,
|
||||
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||
Match::Thread(_) => 1.,
|
||||
Match::Symbol(_) => 1.,
|
||||
Match::Fetch(_) => 1.,
|
||||
}
|
||||
}
|
||||
Mode(ContextPickerMode),
|
||||
}
|
||||
|
||||
fn search(
|
||||
@@ -144,54 +126,19 @@ fn search(
|
||||
matches.extend(
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.into_iter()
|
||||
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
|
||||
.map(Match::Mode),
|
||||
);
|
||||
|
||||
Task::ready(matches)
|
||||
} else {
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
|
||||
let modes = supported_context_picker_modes(&thread_store);
|
||||
let mode_candidates = modes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut matches = search_files_task
|
||||
search_files_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::File)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mode_matches = fuzzy::match_strings(
|
||||
&mode_candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Arc::new(AtomicBool::default()),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.extend(mode_matches.into_iter().map(|mat| {
|
||||
Match::Mode(ModeMatch {
|
||||
mode: modes[mat.candidate_id],
|
||||
mat: Some(mat),
|
||||
})
|
||||
}));
|
||||
|
||||
matches.sort_by(|a, b| {
|
||||
b.score()
|
||||
.partial_cmp(&a.score())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
matches
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -601,7 +548,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
Match::Mode(ModeMatch { mode, .. }) => {
|
||||
Match::Mode(mode) => {
|
||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,23 +2,22 @@ use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::format_context_as_string;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, MultiBuffer,
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle,
|
||||
MultiBuffer,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
|
||||
linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
@@ -56,8 +55,6 @@ pub struct MessageEditor {
|
||||
edits_expanded: bool,
|
||||
editor_is_expanded: bool,
|
||||
waiting_for_summaries_to_send: bool,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -132,18 +129,8 @@ impl MessageEditor {
|
||||
let incompatible_tools =
|
||||
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
cx.subscribe(&editor, |this, _, event, cx| match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
this.message_or_context_changed(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
cx.observe(&context_store, |this, _, cx| {
|
||||
this.message_or_context_changed(false, cx);
|
||||
}),
|
||||
];
|
||||
let subscriptions =
|
||||
vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
@@ -169,8 +156,6 @@ impl MessageEditor {
|
||||
waiting_for_summaries_to_send: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
last_estimated_token_count: None,
|
||||
update_token_count_task: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -271,9 +256,6 @@ impl MessageEditor {
|
||||
text
|
||||
});
|
||||
|
||||
self.last_estimated_token_count.take();
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
|
||||
@@ -955,80 +937,6 @@ impl MessageEditor {
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn last_estimated_token_count(&self) -> Option<usize> {
|
||||
self.last_estimated_token_count
|
||||
}
|
||||
|
||||
pub fn is_waiting_to_update_token_count(&self) -> bool {
|
||||
self.update_token_count_task.is_some()
|
||||
}
|
||||
|
||||
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Changed);
|
||||
self.update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
self.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
|
||||
let context_store = self.context_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let thread = self.thread.clone();
|
||||
|
||||
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let context = context_store.read(cx).context().iter();
|
||||
let new_context = thread.read(cx).filter_new_context(context);
|
||||
let context_text =
|
||||
format_context_as_string(new_context, cx).unwrap_or(String::new());
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
let content = context_text + &message_text;
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
cache: false,
|
||||
}],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
this.update_token_count_task.take();
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
pub enum MessageEditorEvent {
|
||||
EstimatedTokenCount,
|
||||
Changed,
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
@@ -1041,7 +949,6 @@ impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let thread = self.thread.read(cx);
|
||||
let total_token_usage = thread.total_token_usage(cx);
|
||||
let token_usage_ratio = total_token_usage.ratio();
|
||||
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
@@ -1090,8 +997,15 @@ impl Render for MessageEditor {
|
||||
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||
})
|
||||
.child(self.render_editor(font_size, line_height, window, cx))
|
||||
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
|
||||
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
|
||||
})
|
||||
.when(
|
||||
total_token_usage.ratio != TokenUsageRatio::Normal,
|
||||
|parent| {
|
||||
parent.child(self.render_token_limit_callout(
|
||||
line_height,
|
||||
total_token_usage.ratio,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
@@ -18,13 +18,12 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, TokenUsage,
|
||||
};
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -227,33 +226,7 @@ pub enum DetailedSummaryState {
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: usize,
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
impl TotalTokenUsage {
|
||||
pub fn ratio(&self) -> TokenUsageRatio {
|
||||
#[cfg(debug_assertions)]
|
||||
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
|
||||
.unwrap_or("0.8".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
|
||||
if self.total >= self.max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if self.total as f32 / self.max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
} else {
|
||||
TokenUsageRatio::Normal
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
|
||||
TotalTokenUsage {
|
||||
total: self.total + tokens,
|
||||
max: self.max,
|
||||
}
|
||||
}
|
||||
pub ratio: TokenUsageRatio,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
@@ -287,7 +260,6 @@ pub struct Thread {
|
||||
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
|
||||
pending_checkpoint: Option<ThreadCheckpoint>,
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
request_token_usage: Vec<TokenUsage>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
exceeded_window_error: Option<ExceededWindowError>,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
@@ -338,7 +310,6 @@ impl Thread {
|
||||
.spawn(async move { Some(project_snapshot.await) })
|
||||
.shared()
|
||||
},
|
||||
request_token_usage: Vec::new(),
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
exceeded_window_error: None,
|
||||
feedback: None,
|
||||
@@ -406,7 +377,6 @@ impl Thread {
|
||||
tool_use,
|
||||
action_log: cx.new(|_| ActionLog::new(project)),
|
||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||
request_token_usage: serialized.request_token_usage,
|
||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||
exceeded_window_error: None,
|
||||
feedback: None,
|
||||
@@ -660,30 +630,10 @@ impl Thread {
|
||||
self.tool_use.tool_result(id)
|
||||
}
|
||||
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
Some(&self.tool_use.tool_result(id)?.content)
|
||||
}
|
||||
|
||||
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
|
||||
self.tool_use.tool_result_card(id).cloned()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
/// Filter out contexts that have already been included in previous messages
|
||||
pub fn filter_new_context<'a>(
|
||||
&self,
|
||||
context: impl Iterator<Item = &'a AssistantContext>,
|
||||
) -> impl Iterator<Item = &'a AssistantContext> {
|
||||
context.filter(|ctx| self.is_context_new(ctx))
|
||||
}
|
||||
|
||||
fn is_context_new(&self, context: &AssistantContext) -> bool {
|
||||
!self.context.contains_key(&context.id())
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
@@ -695,9 +645,10 @@ impl Thread {
|
||||
|
||||
let message_id = self.insert_message(Role::User, vec![MessageSegment::Text(text)], cx);
|
||||
|
||||
// Filter out contexts that have already been included in previous messages
|
||||
let new_context: Vec<_> = context
|
||||
.into_iter()
|
||||
.filter(|ctx| self.is_context_new(ctx))
|
||||
.filter(|ctx| !self.context.contains_key(&ctx.id()))
|
||||
.collect();
|
||||
|
||||
if !new_context.is_empty() {
|
||||
@@ -877,7 +828,6 @@ impl Thread {
|
||||
.collect(),
|
||||
initial_project_snapshot,
|
||||
cumulative_token_usage: this.cumulative_token_usage,
|
||||
request_token_usage: this.request_token_usage.clone(),
|
||||
detailed_summary_state: this.detailed_summary_state.clone(),
|
||||
exceeded_window_error: this.exceeded_window_error.clone(),
|
||||
})
|
||||
@@ -1063,6 +1013,7 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion(request, &cx);
|
||||
let initial_token_usage =
|
||||
@@ -1088,7 +1039,6 @@ impl Thread {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
|
||||
thread.update_token_usage_at_last_message(token_usage);
|
||||
thread.cumulative_token_usage = thread.cumulative_token_usage
|
||||
+ token_usage
|
||||
- current_token_usage;
|
||||
@@ -1200,12 +1150,6 @@ impl Thread {
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::MaxMonthlySpendReached,
|
||||
));
|
||||
} else if let Some(error) =
|
||||
error.downcast_ref::<ModelRequestLimitReachedError>()
|
||||
{
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::ModelRequestLimitReached { plan: error.plan },
|
||||
));
|
||||
} else if let Some(known_error) =
|
||||
error.downcast_ref::<LanguageModelKnownError>()
|
||||
{
|
||||
@@ -1475,12 +1419,6 @@ impl Thread {
|
||||
)
|
||||
};
|
||||
|
||||
// Store the card separately if it exists
|
||||
if let Some(card) = tool_result.card.clone() {
|
||||
self.tool_use
|
||||
.insert_tool_result_card(tool_use_id.clone(), card);
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
async move |thread: WeakEntity<Thread>, cx| {
|
||||
let output = tool_result.output.await;
|
||||
@@ -1863,14 +1801,14 @@ impl Thread {
|
||||
.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
|
||||
}
|
||||
|
||||
pub fn reject_edits_in_ranges(
|
||||
pub fn reject_edits_in_range(
|
||||
&mut self,
|
||||
buffer: Entity<language::Buffer>,
|
||||
buffer_ranges: Vec<Range<language::Anchor>>,
|
||||
buffer_range: Range<language::Anchor>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.action_log.update(cx, |action_log, cx| {
|
||||
action_log.reject_edits_in_ranges(buffer, buffer_ranges, cx)
|
||||
action_log.reject_edits_in_range(buffer, buffer_range, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1930,35 +1868,6 @@ impl Thread {
|
||||
self.cumulative_token_usage
|
||||
}
|
||||
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
|
||||
let index = self
|
||||
.messages
|
||||
.iter()
|
||||
.position(|msg| msg.id == message_id)
|
||||
.unwrap_or(0);
|
||||
|
||||
if index == 0 {
|
||||
return TotalTokenUsage { total: 0, max };
|
||||
}
|
||||
|
||||
let token_usage = &self
|
||||
.request_token_usage
|
||||
.get(index - 1)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
TotalTokenUsage {
|
||||
total: token_usage.total_tokens() as usize,
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.default_model() else {
|
||||
@@ -1972,33 +1881,30 @@ impl Thread {
|
||||
return TotalTokenUsage {
|
||||
total: exceeded_error.token_count,
|
||||
max,
|
||||
ratio: TokenUsageRatio::Exceeded,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let total = self
|
||||
.token_usage_at_last_message()
|
||||
.unwrap_or_default()
|
||||
.total_tokens() as usize;
|
||||
#[cfg(debug_assertions)]
|
||||
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
|
||||
.unwrap_or("0.8".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
|
||||
TotalTokenUsage { total, max }
|
||||
}
|
||||
let total = self.cumulative_token_usage.total_tokens() as usize;
|
||||
|
||||
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
|
||||
self.request_token_usage
|
||||
.get(self.messages.len().saturating_sub(1))
|
||||
.or_else(|| self.request_token_usage.last())
|
||||
.cloned()
|
||||
}
|
||||
let ratio = if total >= max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if total as f32 / max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
} else {
|
||||
TokenUsageRatio::Normal
|
||||
};
|
||||
|
||||
fn update_token_usage_at_last_message(&mut self, token_usage: TokenUsage) {
|
||||
let placeholder = self.token_usage_at_last_message().unwrap_or_default();
|
||||
self.request_token_usage
|
||||
.resize(self.messages.len(), placeholder);
|
||||
|
||||
if let Some(last) = self.request_token_usage.last_mut() {
|
||||
*last = token_usage;
|
||||
}
|
||||
TotalTokenUsage { total, max, ratio }
|
||||
}
|
||||
|
||||
pub fn deny_tool_use(
|
||||
@@ -2023,8 +1929,6 @@ pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
#[error("Max monthly spend reached")]
|
||||
MaxMonthlySpendReached,
|
||||
#[error("Model request limit reached")]
|
||||
ModelRequestLimitReached { plan: Plan },
|
||||
#[error("Message {header}: {message}")]
|
||||
Message {
|
||||
header: SharedString,
|
||||
|
||||
@@ -509,8 +509,6 @@ pub struct SerializedThread {
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: TokenUsage,
|
||||
#[serde(default)]
|
||||
pub request_token_usage: Vec<TokenUsage>,
|
||||
#[serde(default)]
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
#[serde(default)]
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
@@ -599,7 +597,6 @@ impl LegacySerializedThread {
|
||||
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
|
||||
initial_project_snapshot: self.initial_project_snapshot,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
@@ -27,7 +27,26 @@ pub struct ToolUse {
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
@@ -35,9 +54,10 @@ pub struct ToolUseState {
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
@@ -46,7 +66,6 @@ impl ToolUseState {
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,18 +257,6 @@ impl ToolUseState {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: AnyToolCard,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
|
||||
@@ -191,12 +191,15 @@ impl RenderOnce for ContextPill {
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
icon_path: _,
|
||||
kind: _,
|
||||
kind,
|
||||
focused,
|
||||
on_click,
|
||||
} => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.border_dashed()
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
@@ -204,17 +207,30 @@ impl RenderOnce for ContextPill {
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
div().px_0p5().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl
|
||||
| ContextKind::Symbol => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::XSmall)
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ use buffer_diff::BufferDiff;
|
||||
use collections::BTreeMap;
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
||||
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use language::{Anchor, Buffer, BufferEvent, DiskState, Point};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
@@ -363,10 +363,10 @@ impl ActionLog {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject_edits_in_ranges(
|
||||
pub fn reject_edits_in_range(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_ranges: Vec<Range<impl language::ToPoint>>,
|
||||
buffer_range: Range<impl language::ToPoint>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||
@@ -403,15 +403,29 @@ impl ActionLog {
|
||||
}
|
||||
TrackedBufferStatus::Modified => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let mut buffer_row_ranges = buffer_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
range.start.to_point(buffer).row..range.end.to_point(buffer).row
|
||||
})
|
||||
.peekable();
|
||||
let buffer_range =
|
||||
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
|
||||
|
||||
let mut edits_to_revert = Vec::new();
|
||||
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||
if buffer_range.end.row < edit.new.start {
|
||||
break;
|
||||
} else if buffer_range.start.row > edit.new.end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
));
|
||||
let old_text = tracked_buffer
|
||||
.base_text
|
||||
.chunks_in_range(old_range)
|
||||
.collect::<String>();
|
||||
|
||||
let new_range = tracked_buffer
|
||||
.snapshot
|
||||
.anchor_before(Point::new(edit.new.start, 0))
|
||||
@@ -419,35 +433,7 @@ impl ActionLog {
|
||||
Point::new(edit.new.end, 0),
|
||||
tracked_buffer.snapshot.max_point(),
|
||||
));
|
||||
let new_row_range = new_range.start.to_point(buffer).row
|
||||
..new_range.end.to_point(buffer).row;
|
||||
|
||||
let mut revert = false;
|
||||
while let Some(buffer_row_range) = buffer_row_ranges.peek() {
|
||||
if buffer_row_range.end < new_row_range.start {
|
||||
buffer_row_ranges.next();
|
||||
} else if buffer_row_range.start > new_row_range.end {
|
||||
break;
|
||||
} else {
|
||||
revert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if revert {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
));
|
||||
let old_text = tracked_buffer
|
||||
.base_text
|
||||
.chunks_in_range(old_range)
|
||||
.collect::<String>();
|
||||
edits_to_revert.push((new_range, old_text));
|
||||
}
|
||||
edits_to_revert.push((new_range, old_text));
|
||||
}
|
||||
|
||||
buffer.edit(edits_to_revert, None, cx);
|
||||
@@ -613,7 +599,6 @@ fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edi
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum ChangeAuthor {
|
||||
User,
|
||||
Agent,
|
||||
@@ -1150,48 +1135,9 @@ mod tests {
|
||||
)]
|
||||
);
|
||||
|
||||
// If the rejected range doesn't overlap with any hunk, we ignore it.
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(4, 0)..Point::new(4, 0)],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abc\ndE\nXYZf\nghi\njkl\nmnO"
|
||||
);
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(3, 0),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(5, 0)..Point::new(5, 3),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(0, 0)..Point::new(1, 0)],
|
||||
cx,
|
||||
)
|
||||
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1214,11 +1160,7 @@ mod tests {
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(4, 0)..Point::new(4, 0)],
|
||||
cx,
|
||||
)
|
||||
log.reject_edits_in_range(buffer.clone(), Point::new(4, 0)..Point::new(4, 0), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1230,82 +1172,6 @@ mod tests {
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
|
||||
.unwrap()
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
|
||||
.unwrap()
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abc\ndE\nXYZf\nghi\njkl\nmnO"
|
||||
);
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![
|
||||
HunkStatus {
|
||||
range: Point::new(1, 0)..Point::new(3, 0),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "def\n".into(),
|
||||
},
|
||||
HunkStatus {
|
||||
range: Point::new(5, 0)..Point::new(5, 3),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "mno".into(),
|
||||
}
|
||||
],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
|
||||
..buffer.read(cx).anchor_before(Point::new(1, 0));
|
||||
let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
|
||||
..buffer.read(cx).anchor_before(Point::new(5, 3));
|
||||
|
||||
log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
|
||||
.detach();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abc\ndef\nghi\njkl\nmno"
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"abc\ndef\nghi\njkl\nmno"
|
||||
);
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_reject_deleted_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -1349,11 +1215,7 @@ mod tests {
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(0, 0)..Point::new(0, 0)],
|
||||
cx,
|
||||
)
|
||||
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 0), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1404,11 +1266,7 @@ mod tests {
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(0, 0)..Point::new(0, 11)],
|
||||
cx,
|
||||
)
|
||||
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 11), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1454,7 +1312,7 @@ mod tests {
|
||||
.update(cx, |log, cx| {
|
||||
let range = buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
log::info!("rejecting edits in range {:?}", range);
|
||||
log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
|
||||
log.reject_edits_in_range(buffer.clone(), range, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -9,10 +9,6 @@ use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
@@ -28,87 +24,16 @@ pub fn init(cx: &mut App) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
/// and an optional card view that can be rendered immediately.
|
||||
/// The result of running a tool
|
||||
pub struct ToolResult {
|
||||
/// The asynchronous task that will eventually resolve to the tool's output
|
||||
pub output: Task<Result<String>>,
|
||||
/// An optional view to present the output of the tool.
|
||||
pub card: Option<AnyToolCard>,
|
||||
}
|
||||
|
||||
pub trait ToolCard: 'static + Sized {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
render: fn(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement,
|
||||
}
|
||||
|
||||
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
fn from(entity: Entity<T>) -> Self {
|
||||
fn downcast_render<T: ToolCard>(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let entity = entity.downcast::<T>().unwrap();
|
||||
entity.update(cx, |entity, cx| {
|
||||
entity.render(status, window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
Self {
|
||||
entity: entity.into(),
|
||||
render: downcast_render::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyToolCard {
|
||||
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Task<Result<String>>> for ToolResult {
|
||||
/// Convert from a task to a ToolResult with no card
|
||||
/// Convert from a task to a ToolResult
|
||||
fn from(output: Task<Result<String>>) -> Self {
|
||||
Self { output, card: None }
|
||||
Self { output }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -33,9 +32,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -22,17 +22,14 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_action_tool::CodeActionTool;
|
||||
@@ -59,39 +56,28 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(BatchTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
/// The search term or question to query on the web.
|
||||
query: String,
|
||||
}
|
||||
|
||||
pub struct WebSearchTool;
|
||||
|
||||
impl Tool for WebSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"web_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<WebSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Web Search".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
|
||||
return Task::ready(Err(anyhow!("Web search is not available."))).into();
|
||||
};
|
||||
|
||||
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
|
||||
let output = cx.background_spawn({
|
||||
let search_task = search_task.clone();
|
||||
async move {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
||||
serde_json::to_string(&response).context("Failed to serialize search results")
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebSearchToolCard {
|
||||
response: Option<Result<WebSearchResponse>>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl WebSearchToolCard {
|
||||
fn new(
|
||||
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn(async move |this, cx| {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err));
|
||||
this.update(cx, |this, cx| {
|
||||
this.response = Some(response);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self {
|
||||
response: None,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for WebSearchToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
_status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let header = h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(IconName::Globe)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.citations.len() == 1 {
|
||||
"1 result".into()
|
||||
} else {
|
||||
format!("{} results", response.citations.len()).into()
|
||||
};
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Label::new("Searched the Web").size(LabelSize::Small))
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text),
|
||||
)
|
||||
.child(Label::new(text).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
Some(Err(error)) => div()
|
||||
.id("web-search-error")
|
||||
.child(Label::new("Web Search failed").size(LabelSize::Small))
|
||||
.tooltip(Tooltip::text(error.to_string()))
|
||||
.into_any_element(),
|
||||
|
||||
None => Label::new("Searching the Web…")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"web-search-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
.into_any();
|
||||
|
||||
let content =
|
||||
self.response.as_ref().and_then(|response| match response {
|
||||
Ok(response) => {
|
||||
Some(
|
||||
v_flex()
|
||||
.ml_1p5()
|
||||
.pl_1p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.gap_1()
|
||||
.children(response.citations.iter().enumerate().map(
|
||||
|(index, citation)| {
|
||||
let title = citation.title.clone();
|
||||
let url = citation.url.clone();
|
||||
|
||||
Button::new(("citation", index), title)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.truncate(true)
|
||||
.tooltip({
|
||||
let url = url.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Citation Link",
|
||||
None,
|
||||
url.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
move |_, _, cx| cx.open_url(&url)
|
||||
})
|
||||
},
|
||||
))
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
v_flex().my_2().gap_1().child(header).children(content)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
create table subscription_usages (
|
||||
id serial primary key,
|
||||
user_id integer not null,
|
||||
period_start_at timestamp without time zone not null,
|
||||
period_end_at timestamp without time zone not null,
|
||||
model_requests int not null default 0,
|
||||
edit_predictions int not null default 0
|
||||
);
|
||||
|
||||
create unique index uix_subscription_usages_on_user_id_start_at_end_at on subscription_usages (user_id, period_start_at, period_end_at);
|
||||
@@ -1,4 +0,0 @@
|
||||
alter table subscription_usages
|
||||
add column plan text not null;
|
||||
|
||||
create index ix_subscription_usages_on_plan on subscription_usages (plan);
|
||||
@@ -15,12 +15,10 @@ use stripe::{
|
||||
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
|
||||
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
|
||||
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
|
||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
|
||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
|
||||
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
|
||||
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::db::billing_subscription::{
|
||||
@@ -54,7 +52,6 @@ pub fn router() -> Router {
|
||||
post(manage_billing_subscription),
|
||||
)
|
||||
.route("/billing/monthly_spend", get(get_monthly_spend))
|
||||
.route("/billing/usage", get(get_current_usage))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -162,7 +159,6 @@ struct BillingSubscriptionJson {
|
||||
id: BillingSubscriptionId,
|
||||
name: String,
|
||||
status: StripeSubscriptionStatus,
|
||||
trial_end_at: Option<String>,
|
||||
cancel_at: Option<String>,
|
||||
/// Whether this subscription can be canceled.
|
||||
is_cancelable: bool,
|
||||
@@ -192,21 +188,9 @@ async fn list_billing_subscriptions(
|
||||
id: subscription.id,
|
||||
name: match subscription.kind {
|
||||
Some(SubscriptionKind::ZedPro) => "Zed Pro".to_string(),
|
||||
Some(SubscriptionKind::ZedProTrial) => "Zed Pro (Trial)".to_string(),
|
||||
Some(SubscriptionKind::ZedFree) => "Zed Free".to_string(),
|
||||
None => "Zed LLM Usage".to_string(),
|
||||
},
|
||||
status: subscription.stripe_subscription_status,
|
||||
trial_end_at: if subscription.kind == Some(SubscriptionKind::ZedProTrial) {
|
||||
maybe!({
|
||||
let end_at = subscription.stripe_current_period_end?;
|
||||
let end_at = DateTime::from_timestamp(end_at, 0)?;
|
||||
|
||||
Some(end_at.to_rfc3339_opts(SecondsFormat::Millis, true))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
|
||||
cancel_at
|
||||
.and_utc()
|
||||
@@ -223,7 +207,6 @@ async fn list_billing_subscriptions(
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ProductCode {
|
||||
ZedPro,
|
||||
ZedProTrial,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -303,38 +286,24 @@ async fn create_billing_subscription(
|
||||
customer.id
|
||||
};
|
||||
|
||||
let success_url = format!(
|
||||
"{}/account?checkout_complete=1",
|
||||
app.config.zed_dot_dev_url()
|
||||
);
|
||||
|
||||
let checkout_session_url = match body.product {
|
||||
Some(ProductCode::ZedPro) => {
|
||||
let success_url = format!(
|
||||
"{}/account?checkout_complete=1",
|
||||
app.config.zed_dot_dev_url()
|
||||
);
|
||||
stripe_billing
|
||||
.checkout_with_price(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
&success_url,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Some(ProductCode::ZedProTrial) => {
|
||||
stripe_billing
|
||||
.checkout_with_price(
|
||||
app.config.zed_pro_trial_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
&success_url,
|
||||
)
|
||||
.checkout_with_zed_pro(customer_id, &user.github_login, &success_url)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
let default_model = llm_db.model(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
"claude-3-7-sonnet",
|
||||
)?;
|
||||
let default_model =
|
||||
llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-7-sonnet")?;
|
||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||
let success_url = format!(
|
||||
"{}/account?checkout_complete=1",
|
||||
app.config.zed_dot_dev_url()
|
||||
);
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
.await?
|
||||
@@ -353,8 +322,6 @@ enum ManageSubscriptionIntent {
|
||||
///
|
||||
/// This will open the Stripe billing portal without putting the user in a specific flow.
|
||||
ManageSubscription,
|
||||
/// The user intends to upgrade to Zed Pro.
|
||||
UpgradeToPro,
|
||||
/// The user intends to cancel their subscription.
|
||||
Cancel,
|
||||
/// The user intends to stop the cancellation of their subscription.
|
||||
@@ -406,10 +373,11 @@ async fn manage_billing_subscription(
|
||||
.get_billing_subscription_by_id(body.subscription_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("subscription not found"))?;
|
||||
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
|
||||
.context("failed to parse subscription ID")?;
|
||||
|
||||
if body.intent == ManageSubscriptionIntent::StopCancellation {
|
||||
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
|
||||
.context("failed to parse subscription ID")?;
|
||||
|
||||
let updated_stripe_subscription = Subscription::update(
|
||||
&stripe_client,
|
||||
&subscription_id,
|
||||
@@ -442,47 +410,6 @@ async fn manage_billing_subscription(
|
||||
|
||||
let flow = match body.intent {
|
||||
ManageSubscriptionIntent::ManageSubscription => None,
|
||||
ManageSubscriptionIntent::UpgradeToPro => {
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id()?;
|
||||
let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id()?;
|
||||
|
||||
let stripe_subscription =
|
||||
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
|
||||
|
||||
let subscription_item_to_update = stripe_subscription
|
||||
.items
|
||||
.data
|
||||
.iter()
|
||||
.find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
|
||||
if price.id == zed_free_price_id || price.id == zed_pro_trial_price_id {
|
||||
Some(item.id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("No subscription item to update"))?;
|
||||
|
||||
Some(CreateBillingPortalSessionFlowData {
|
||||
type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm,
|
||||
subscription_update_confirm: Some(
|
||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm {
|
||||
subscription: subscription.stripe_subscription_id,
|
||||
items: vec![
|
||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems {
|
||||
id: subscription_item_to_update.to_string(),
|
||||
price: Some(zed_pro_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
},
|
||||
],
|
||||
discounts: None,
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
ManageSubscriptionIntent::Cancel => Some(CreateBillingPortalSessionFlowData {
|
||||
type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
|
||||
after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
|
||||
@@ -769,25 +696,22 @@ async fn handle_customer_subscription_event(
|
||||
|
||||
log::info!("handling Stripe {} event: {}", event.type_, event.id);
|
||||
|
||||
let subscription_kind = maybe!({
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
|
||||
let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id().ok()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id().ok()?;
|
||||
let subscription_kind =
|
||||
if let Some(zed_pro_price_id) = app.config.stripe_zed_pro_price_id.as_deref() {
|
||||
let has_zed_pro_price = subscription.items.data.iter().any(|item| {
|
||||
item.price
|
||||
.as_ref()
|
||||
.map_or(false, |price| price.id.as_str() == zed_pro_price_id)
|
||||
});
|
||||
|
||||
subscription.items.data.iter().find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
|
||||
if price.id == zed_pro_price_id {
|
||||
if has_zed_pro_price {
|
||||
Some(SubscriptionKind::ZedPro)
|
||||
} else if price.id == zed_pro_trial_price_id {
|
||||
Some(SubscriptionKind::ZedProTrial)
|
||||
} else if price.id == zed_free_price_id {
|
||||
Some(SubscriptionKind::ZedFree)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let billing_customer =
|
||||
find_or_create_billing_customer(app, stripe_client, subscription.customer)
|
||||
@@ -950,105 +874,6 @@ async fn get_monthly_spend(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetCurrentUsageParams {
|
||||
github_user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UsageCounts {
|
||||
pub used: i32,
|
||||
pub limit: Option<i32>,
|
||||
pub remaining: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GetCurrentUsageResponse {
|
||||
pub model_requests: UsageCounts,
|
||||
pub edit_predictions: UsageCounts,
|
||||
}
|
||||
|
||||
async fn get_current_usage(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetCurrentUsageParams>,
|
||||
) -> Result<Json<GetCurrentUsageResponse>> {
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_user_id(params.github_user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
return Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"LLM database not available".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let empty_usage = GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
used: 0,
|
||||
limit: Some(0),
|
||||
remaining: Some(0),
|
||||
},
|
||||
edit_predictions: UsageCounts {
|
||||
used: 0,
|
||||
limit: Some(0),
|
||||
remaining: Some(0),
|
||||
},
|
||||
};
|
||||
|
||||
let Some(subscription) = app.db.get_active_billing_subscription(user.id).await? else {
|
||||
return Ok(Json(empty_usage));
|
||||
};
|
||||
|
||||
let subscription_period = maybe!({
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
let period_end_at = subscription.current_period_end_at()?;
|
||||
|
||||
Some((period_start_at, period_end_at))
|
||||
});
|
||||
|
||||
let Some((period_start_at, period_end_at)) = subscription_period else {
|
||||
return Ok(Json(empty_usage));
|
||||
};
|
||||
|
||||
let usage = llm_db
|
||||
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
|
||||
.await?;
|
||||
let Some(usage) = usage else {
|
||||
return Ok(Json(empty_usage));
|
||||
};
|
||||
|
||||
let plan = match usage.plan {
|
||||
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
|
||||
};
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
let edit_prediction_limit = match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
Ok(Json(GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
used: usage.model_requests,
|
||||
limit: model_requests_limit,
|
||||
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
|
||||
},
|
||||
edit_predictions: UsageCounts {
|
||||
used: usage.edit_predictions,
|
||||
limit: edit_prediction_limit,
|
||||
remaining: edit_prediction_limit.map(|limit| (limit - usage.edit_predictions).max(0)),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
impl From<SubscriptionStatus> for StripeSubscriptionStatus {
|
||||
fn from(value: SubscriptionStatus) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -62,14 +62,11 @@ impl Database {
|
||||
billing_subscription::Entity::update(billing_subscription::ActiveModel {
|
||||
id: ActiveValue::set(id),
|
||||
billing_customer_id: params.billing_customer_id.clone(),
|
||||
kind: params.kind.clone(),
|
||||
stripe_subscription_id: params.stripe_subscription_id.clone(),
|
||||
stripe_subscription_status: params.stripe_subscription_status.clone(),
|
||||
stripe_cancel_at: params.stripe_cancel_at.clone(),
|
||||
stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
|
||||
stripe_current_period_start: params.stripe_current_period_start.clone(),
|
||||
stripe_current_period_end: params.stripe_current_period_end.clone(),
|
||||
created_at: ActiveValue::not_set(),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
@@ -108,28 +105,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_active_billing_subscription(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<billing_subscription::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(billing_customer::Column::UserId.eq(user_id))
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
)
|
||||
.add(billing_subscription::Column::Kind.is_not_null()),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all of the billing subscriptions for the user with the specified ID.
|
||||
///
|
||||
/// Note that this returns the subscriptions regardless of their status.
|
||||
@@ -167,7 +142,6 @@ impl Database {
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
)
|
||||
.filter(billing_subscription::Column::Kind.is_null())
|
||||
.order_by_asc(billing_subscription::Column::Id)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -19,18 +19,6 @@ pub struct Model {
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn current_period_start_at(&self) -> Option<DateTimeUtc> {
|
||||
let period_start = self.stripe_current_period_start?;
|
||||
chrono::DateTime::from_timestamp(period_start, 0)
|
||||
}
|
||||
|
||||
pub fn current_period_end_at(&self) -> Option<DateTimeUtc> {
|
||||
let period_end = self.stripe_current_period_end?;
|
||||
chrono::DateTime::from_timestamp(period_end, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
@@ -55,10 +43,6 @@ impl ActiveModelBehavior for ActiveModel {}
|
||||
pub enum SubscriptionKind {
|
||||
#[sea_orm(string_value = "zed_pro")]
|
||||
ZedPro,
|
||||
#[sea_orm(string_value = "zed_pro_trial")]
|
||||
ZedProTrial,
|
||||
#[sea_orm(string_value = "zed_free")]
|
||||
ZedFree,
|
||||
}
|
||||
|
||||
/// The status of a Stripe subscription.
|
||||
|
||||
@@ -183,8 +183,6 @@ pub struct Config {
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
pub stripe_api_key: Option<String>,
|
||||
pub stripe_zed_pro_price_id: Option<String>,
|
||||
pub stripe_zed_pro_trial_price_id: Option<String>,
|
||||
pub stripe_zed_free_price_id: Option<String>,
|
||||
pub supermaven_admin_api_key: Option<Arc<str>>,
|
||||
pub user_backfiller_github_access_token: Option<Arc<str>>,
|
||||
}
|
||||
@@ -203,29 +201,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zed_pro_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
pub fn zed_pro_trial_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id(
|
||||
"Zed Pro Trial",
|
||||
self.stripe_zed_pro_trial_price_id.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
fn parse_stripe_price_id(name: &str, value: Option<&str>) -> anyhow::Result<stripe::PriceId> {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
let price_id = value.ok_or_else(|| anyhow!("{name} price ID not set"))?;
|
||||
|
||||
Ok(stripe::PriceId::from_str(price_id)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test() -> Self {
|
||||
Self {
|
||||
@@ -264,8 +239,6 @@ impl Config {
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_zed_pro_price_id: None,
|
||||
stripe_zed_pro_trial_price_id: None,
|
||||
stripe_zed_free_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
@@ -351,9 +324,12 @@ impl AppState {
|
||||
llm_db,
|
||||
livekit_client,
|
||||
blob_store_client: build_blob_store_client(&config).await.log_err(),
|
||||
stripe_billing: stripe_client
|
||||
.clone()
|
||||
.map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
|
||||
stripe_billing: stripe_client.clone().map(|stripe_client| {
|
||||
Arc::new(StripeBilling::new(
|
||||
stripe_client,
|
||||
config.stripe_zed_pro_price_id.clone(),
|
||||
))
|
||||
}),
|
||||
stripe_client,
|
||||
rate_limiter: Arc::new(RateLimiter::new(db)),
|
||||
executor,
|
||||
|
||||
@@ -8,9 +8,9 @@ mod tests;
|
||||
|
||||
use collections::HashMap;
|
||||
pub use ids::*;
|
||||
use rpc::LanguageModelProvider;
|
||||
pub use seed::*;
|
||||
pub use tables::*;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestLlmDb;
|
||||
|
||||
@@ -2,5 +2,4 @@ use super::*;
|
||||
|
||||
pub mod billing_events;
|
||||
pub mod providers;
|
||||
pub mod subscription_usages;
|
||||
pub mod usages;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::db::UserId;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl LlmDatabase {
|
||||
pub async fn get_subscription_usage_for_period(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(subscription_usage::Entity::find()
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
|
||||
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,5 @@ pub mod billing_event;
|
||||
pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
pub mod subscription_usage;
|
||||
pub mod usage;
|
||||
pub mod usage_measure;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::db::UserId;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "subscription_usages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub user_id: UserId,
|
||||
pub period_start_at: PrimitiveDateTime,
|
||||
pub period_end_at: PrimitiveDateTime,
|
||||
pub plan: SubscriptionKind,
|
||||
pub model_requests: i32,
|
||||
pub edit_predictions: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,5 +1,5 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
use rpc::LanguageModelProvider;
|
||||
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::Cents;
|
||||
use crate::db::{billing_subscription, user};
|
||||
use crate::db::user;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -8,9 +8,7 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::Plan;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -26,12 +24,11 @@ pub struct LlmTokenClaims {
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
pub bypass_account_age_check: bool,
|
||||
pub has_predict_edits_feature_flag: bool,
|
||||
pub has_llm_subscription: bool,
|
||||
pub max_monthly_spend_in_cents: u32,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
pub plan: rpc::proto::Plan,
|
||||
}
|
||||
|
||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
@@ -42,9 +39,8 @@ impl LlmTokenClaims {
|
||||
is_staff: bool,
|
||||
billing_preferences: Option<billing_preference::Model>,
|
||||
feature_flags: &Vec<String>,
|
||||
has_legacy_llm_subscription: bool,
|
||||
has_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
subscription: Option<billing_subscription::Model>,
|
||||
system_id: Option<String>,
|
||||
config: &Config,
|
||||
) -> Result<String> {
|
||||
@@ -70,7 +66,10 @@ impl LlmTokenClaims {
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
has_llm_subscription: has_legacy_llm_subscription,
|
||||
has_predict_edits_feature_flag: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "predict-edits"),
|
||||
has_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
preferences.max_monthly_llm_usage_spending_in_cents as u32
|
||||
@@ -78,18 +77,7 @@ impl LlmTokenClaims {
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan: match plan {
|
||||
rpc::proto::Plan::Free => Plan::Free,
|
||||
rpc::proto::Plan::ZedPro => Plan::ZedPro,
|
||||
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
},
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
let period_end_at = subscription.current_period_end_at()?;
|
||||
|
||||
Some((period_start_at.naive_utc(), period_end_at.naive_utc()))
|
||||
}),
|
||||
plan,
|
||||
};
|
||||
|
||||
Ok(jsonwebtoken::encode(
|
||||
|
||||
@@ -3707,9 +3707,7 @@ async fn count_language_model_tokens(
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
|
||||
proto::Plan::Free | proto::Plan::ZedProTrial => {
|
||||
Box::new(FreeCountLanguageModelTokensRateLimit)
|
||||
}
|
||||
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
|
||||
};
|
||||
|
||||
session
|
||||
@@ -3829,7 +3827,7 @@ async fn compute_embeddings(
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
};
|
||||
|
||||
session
|
||||
@@ -4137,8 +4135,7 @@ async fn get_llm_api_token(
|
||||
Err(anyhow!("terms of service not accepted"))?
|
||||
}
|
||||
|
||||
let has_legacy_llm_subscription = session.has_llm_subscription(&db).await?;
|
||||
let billing_subscription = db.get_active_billing_subscription(user.id).await?;
|
||||
let has_llm_subscription = session.has_llm_subscription(&db).await?;
|
||||
let billing_preferences = db.get_billing_preferences(user.id).await?;
|
||||
|
||||
let token = LlmTokenClaims::create(
|
||||
@@ -4146,9 +4143,8 @@ async fn get_llm_api_token(
|
||||
session.is_staff(),
|
||||
billing_preferences,
|
||||
&flags,
|
||||
has_legacy_llm_subscription,
|
||||
has_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
billing_subscription,
|
||||
session.system_id.clone(),
|
||||
&session.app_state.config,
|
||||
)?;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Cents, Result, llm};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use chrono::{Datelike, Utc};
|
||||
use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stripe::PriceId;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct StripeBilling {
|
||||
state: RwLock<StripeBillingState>,
|
||||
client: Arc<stripe::Client>,
|
||||
zed_pro_price_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -32,10 +32,11 @@ struct StripeBillingPrice {
|
||||
}
|
||||
|
||||
impl StripeBilling {
|
||||
pub fn new(client: Arc<stripe::Client>) -> Self {
|
||||
pub fn new(client: Arc<stripe::Client>, zed_pro_price_id: Option<String>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
state: RwLock::default(),
|
||||
zed_pro_price_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,19 +385,23 @@ impl StripeBilling {
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
|
||||
pub async fn checkout_with_price(
|
||||
pub async fn checkout_with_zed_pro(
|
||||
&self,
|
||||
price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let zed_pro_price_id = self
|
||||
.zed_pro_price_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Zed Pro price ID not set"))?;
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
price: Some(zed_pro_price_id.clone()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}]);
|
||||
|
||||
@@ -558,8 +558,6 @@ impl TestServer {
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_zed_pro_price_id: None,
|
||||
stripe_zed_pro_trial_price_id: None,
|
||||
stripe_zed_free_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Ok, Result, anyhow};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
@@ -256,21 +256,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
self.name()
|
||||
);
|
||||
delegate.update_status(self.name(), DapStatus::Downloading);
|
||||
match self.install_binary(version, delegate).await {
|
||||
Ok(_) => {
|
||||
delegate.update_status(self.name(), DapStatus::None);
|
||||
}
|
||||
Err(error) => {
|
||||
delegate.update_status(
|
||||
self.name(),
|
||||
DapStatus::Failed {
|
||||
error: error.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
self.install_binary(version, delegate).await?;
|
||||
|
||||
delegate
|
||||
.updated_adapters()
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod transport;
|
||||
|
||||
pub use dap_types::*;
|
||||
pub use registry::DapRegistry;
|
||||
pub use task::DebugRequestType;
|
||||
pub use task::{DebugAdapterConfig, DebugRequestType};
|
||||
|
||||
pub type ScopeId = u64;
|
||||
pub type VariableReference = u64;
|
||||
|
||||
@@ -12,9 +12,8 @@ use dap::{
|
||||
};
|
||||
use futures::{SinkExt as _, channel::mpsc};
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
|
||||
actions, anchored, deferred,
|
||||
Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity, actions,
|
||||
};
|
||||
|
||||
use project::{
|
||||
@@ -33,7 +32,6 @@ use std::sync::Arc;
|
||||
use task::DebugTaskDefinition;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use util::debug_panic;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -66,7 +64,6 @@ pub struct DebugPanel {
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -129,7 +126,6 @@ impl DebugPanel {
|
||||
focus_handle: cx.focus_handle(),
|
||||
project: project.downgrade(),
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
};
|
||||
|
||||
debug_panel
|
||||
@@ -317,20 +313,8 @@ impl DebugPanel {
|
||||
.any(|item| item.read(cx).session_id(cx) == session_id)
|
||||
{
|
||||
// We already have an item for this session.
|
||||
debug_panic!("We should never reuse session ids");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
project,
|
||||
this.workspace.clone(),
|
||||
@@ -454,13 +438,7 @@ impl DebugPanel {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
session.update(cx, |this, cx| {
|
||||
if let Some(running) = this.mode().as_running() {
|
||||
running.update(cx, |this, cx| {
|
||||
this.serialize_layout(window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let session_id = session.update(cx, |this, cx| this.session_id(cx));
|
||||
let should_prompt = self
|
||||
.project
|
||||
@@ -589,57 +567,6 @@ impl DebugPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(running_state) = self
|
||||
.active_session
|
||||
.as_ref()
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
{
|
||||
let pane_items_status = running_state.read(cx).pane_items_status(cx);
|
||||
let this = cx.weak_entity();
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
|
||||
for (item_kind, is_visible) in pane_items_status.into_iter() {
|
||||
menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
|
||||
let this = this.clone();
|
||||
move |window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(running_state) =
|
||||
this.active_session.as_ref().and_then(|session| {
|
||||
session.read(cx).mode().as_running().cloned()
|
||||
})
|
||||
{
|
||||
running_state.update(cx, |state, cx| {
|
||||
if is_visible {
|
||||
state.remove_pane_item(item_kind, window, cx);
|
||||
} else {
|
||||
state.add_pane_item(item_kind, position, window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
});
|
||||
self.context_menu = Some((context_menu, position, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let active_session = self.active_session.clone();
|
||||
|
||||
@@ -782,6 +709,9 @@ impl DebugPanel {
|
||||
this.restart_session(cx);
|
||||
},
|
||||
))
|
||||
.disabled(
|
||||
!capabilities.supports_restart_request.unwrap_or_default(),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Restart")(window, cx)
|
||||
}),
|
||||
@@ -961,49 +891,11 @@ impl Render for DebugPanel {
|
||||
let has_sessions = self.sessions.len() > 0;
|
||||
debug_assert_eq!(has_sessions, self.active_session.is_some());
|
||||
|
||||
if self
|
||||
.active_session
|
||||
.as_ref()
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
.map(|state| state.read(cx).has_open_context_menu(cx))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.context_menu.take();
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.key_context("DebugPanel")
|
||||
.child(h_flex().children(self.top_controls_strip(window, cx)))
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.when(self.active_session.is_some(), |this| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(|this, event: &MouseDownEvent, window, cx| {
|
||||
if this
|
||||
.active_session
|
||||
.as_ref()
|
||||
.and_then(|session| {
|
||||
session.read(cx).mode().as_running().map(|state| {
|
||||
state.read(cx).has_pane_at_position(event.position)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
this.deploy_context_menu(event.position, window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
})
|
||||
.map(|this| {
|
||||
if has_sessions {
|
||||
this.children(self.active_session.clone())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use collections::HashMap;
|
||||
use dap::Capabilities;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
|
||||
use project::Project;
|
||||
@@ -10,43 +9,19 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
|
||||
|
||||
use crate::session::running::{
|
||||
self, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
|
||||
loaded_source_list::LoadedSourceList, module_list::ModuleList,
|
||||
stack_frame_list::StackFrameList, variable_list::VariableList,
|
||||
module_list::ModuleList, stack_frame_list::StackFrameList, variable_list::VariableList,
|
||||
};
|
||||
|
||||
#[derive(Clone, Hash, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) enum DebuggerPaneItem {
|
||||
Console,
|
||||
Variables,
|
||||
BreakpointList,
|
||||
Frames,
|
||||
Modules,
|
||||
LoadedSources,
|
||||
}
|
||||
|
||||
impl DebuggerPaneItem {
|
||||
pub(crate) fn all() -> &'static [DebuggerPaneItem] {
|
||||
static VARIANTS: &[DebuggerPaneItem] = &[
|
||||
DebuggerPaneItem::Console,
|
||||
DebuggerPaneItem::Variables,
|
||||
DebuggerPaneItem::BreakpointList,
|
||||
DebuggerPaneItem::Frames,
|
||||
DebuggerPaneItem::Modules,
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
];
|
||||
VARIANTS
|
||||
}
|
||||
|
||||
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
|
||||
match self {
|
||||
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
|
||||
DebuggerPaneItem::LoadedSources => capabilities
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or_default(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_shared_string(self) -> SharedString {
|
||||
match self {
|
||||
DebuggerPaneItem::Console => SharedString::new_static("Console"),
|
||||
@@ -54,17 +29,10 @@ impl DebuggerPaneItem {
|
||||
DebuggerPaneItem::BreakpointList => SharedString::new_static("Breakpoints"),
|
||||
DebuggerPaneItem::Frames => SharedString::new_static("Frames"),
|
||||
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
|
||||
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DebuggerPaneItem> for SharedString {
|
||||
fn from(item: DebuggerPaneItem) -> Self {
|
||||
item.to_shared_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedAxis(pub Axis);
|
||||
|
||||
@@ -168,7 +136,6 @@ pub(crate) fn deserialize_pane_layout(
|
||||
module_list: &Entity<ModuleList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoint_list: &Entity<BreakpointList>,
|
||||
loaded_sources: &Entity<LoadedSourceList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<RunningState>,
|
||||
@@ -190,7 +157,6 @@ pub(crate) fn deserialize_pane_layout(
|
||||
module_list,
|
||||
console,
|
||||
breakpoint_list,
|
||||
loaded_sources,
|
||||
subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -225,7 +191,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||
.iter()
|
||||
.map(|child| match child {
|
||||
DebuggerPaneItem::Frames => Box::new(SubView::new(
|
||||
stack_frame_list.focus_handle(cx),
|
||||
pane.focus_handle(cx),
|
||||
stack_frame_list.clone().into(),
|
||||
DebuggerPaneItem::Frames,
|
||||
None,
|
||||
@@ -246,19 +212,13 @@ pub(crate) fn deserialize_pane_layout(
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Modules => Box::new(SubView::new(
|
||||
module_list.focus_handle(cx),
|
||||
pane.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
|
||||
loaded_sources.focus_handle(cx),
|
||||
loaded_sources.clone().into(),
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
|
||||
DebuggerPaneItem::Console => Box::new(SubView::new(
|
||||
pane.focus_handle(cx),
|
||||
console.clone().into(),
|
||||
|
||||
@@ -11,12 +11,12 @@ use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
use breakpoint_list::BreakpointList;
|
||||
use collections::{HashMap, IndexMap};
|
||||
use collections::HashMap;
|
||||
use console::Console;
|
||||
use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
|
||||
use gpui::{
|
||||
Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
NoAction, Pixels, Point, Subscription, Task, WeakEntity,
|
||||
NoAction, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
@@ -49,10 +49,8 @@ pub struct RunningState {
|
||||
variable_list: Entity<variable_list::VariableList>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
stack_frame_list: Entity<stack_frame_list::StackFrameList>,
|
||||
loaded_sources_list: Entity<LoadedSourceList>,
|
||||
module_list: Entity<module_list::ModuleList>,
|
||||
_module_list: Entity<module_list::ModuleList>,
|
||||
_console: Entity<Console>,
|
||||
breakpoint_list: Entity<BreakpointList>,
|
||||
panes: PaneGroup,
|
||||
pane_close_subscriptions: HashMap<EntityId, Subscription>,
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
@@ -385,6 +383,7 @@ impl RunningState {
|
||||
|
||||
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
|
||||
|
||||
#[expect(unused)]
|
||||
let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
|
||||
|
||||
let console = cx.new(|cx| {
|
||||
@@ -397,7 +396,7 @@ impl RunningState {
|
||||
)
|
||||
});
|
||||
|
||||
let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
|
||||
let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.observe(&module_list, |_, _, cx| cx.notify()),
|
||||
@@ -422,9 +421,6 @@ impl RunningState {
|
||||
}
|
||||
cx.notify()
|
||||
}),
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
this.serialize_layout(window, cx);
|
||||
}),
|
||||
];
|
||||
|
||||
let mut pane_close_subscriptions = HashMap::default();
|
||||
@@ -437,8 +433,7 @@ impl RunningState {
|
||||
&variable_list,
|
||||
&module_list,
|
||||
&console,
|
||||
&breakpoint_list,
|
||||
&loaded_source_list,
|
||||
&breakpoints,
|
||||
&mut pane_close_subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -454,7 +449,7 @@ impl RunningState {
|
||||
&variable_list,
|
||||
&module_list,
|
||||
&console,
|
||||
&breakpoint_list,
|
||||
breakpoints,
|
||||
&mut pane_close_subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -474,140 +469,14 @@ impl RunningState {
|
||||
stack_frame_list,
|
||||
session_id,
|
||||
panes,
|
||||
module_list,
|
||||
_module_list: module_list,
|
||||
_console: console,
|
||||
breakpoint_list,
|
||||
loaded_sources_list: loaded_source_list,
|
||||
pane_close_subscriptions,
|
||||
_schedule_serialize: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_pane_item(
|
||||
&mut self,
|
||||
item_kind: DebuggerPaneItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(
|
||||
item_kind.is_supported(self.session.read(cx).capabilities()),
|
||||
"We should only allow removing supported item kinds"
|
||||
);
|
||||
|
||||
if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
|
||||
Some(pane).zip(
|
||||
pane.read(cx)
|
||||
.items()
|
||||
.find(|item| {
|
||||
item.act_as::<SubView>(cx)
|
||||
.is_some_and(|view| view.read(cx).kind == item_kind)
|
||||
})
|
||||
.map(|item| item.item_id()),
|
||||
)
|
||||
}) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.remove_item(item_id, false, true, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_pane_at_position(&self, position: Point<Pixels>) -> bool {
|
||||
self.panes.pane_at_pixel_position(position).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn add_pane_item(
|
||||
&mut self,
|
||||
item_kind: DebuggerPaneItem,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(
|
||||
item_kind.is_supported(self.session.read(cx).capabilities()),
|
||||
"We should only allow adding supported item kinds"
|
||||
);
|
||||
|
||||
if let Some(pane) = self.panes.pane_at_pixel_position(position) {
|
||||
let sub_view = match item_kind {
|
||||
DebuggerPaneItem::Console => {
|
||||
let weak_console = self._console.clone().downgrade();
|
||||
|
||||
Box::new(SubView::new(
|
||||
pane.focus_handle(cx),
|
||||
self._console.clone().into(),
|
||||
item_kind,
|
||||
Some(Box::new(move |cx| {
|
||||
weak_console
|
||||
.read_with(cx, |console, cx| console.show_indicator(cx))
|
||||
.unwrap_or_default()
|
||||
})),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
DebuggerPaneItem::Variables => Box::new(SubView::new(
|
||||
self.variable_list.focus_handle(cx),
|
||||
self.variable_list.clone().into(),
|
||||
item_kind,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
|
||||
self.breakpoint_list.focus_handle(cx),
|
||||
self.breakpoint_list.clone().into(),
|
||||
item_kind,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Frames => Box::new(SubView::new(
|
||||
self.stack_frame_list.focus_handle(cx),
|
||||
self.stack_frame_list.clone().into(),
|
||||
item_kind,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Modules => Box::new(SubView::new(
|
||||
self.module_list.focus_handle(cx),
|
||||
self.module_list.clone().into(),
|
||||
item_kind,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
|
||||
self.loaded_sources_list.focus_handle(cx),
|
||||
self.loaded_sources_list.clone().into(),
|
||||
item_kind,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
};
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(sub_view, false, false, None, window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
|
||||
let caps = self.session.read(cx).capabilities();
|
||||
let mut pane_item_status = IndexMap::from_iter(
|
||||
DebuggerPaneItem::all()
|
||||
.iter()
|
||||
.filter(|kind| kind.is_supported(&caps))
|
||||
.map(|kind| (*kind, false)),
|
||||
);
|
||||
self.panes.panes().iter().for_each(|pane| {
|
||||
pane.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.act_as::<SubView>(cx))
|
||||
.for_each(|view| {
|
||||
pane_item_status.insert(view.read(cx).kind, true);
|
||||
});
|
||||
});
|
||||
|
||||
pane_item_status
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self._schedule_serialize.is_none() {
|
||||
self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
@@ -661,10 +530,6 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
|
||||
self.variable_list.read(cx).has_open_context_menu()
|
||||
}
|
||||
|
||||
pub fn session(&self) -> &Entity<Session> {
|
||||
&self.session
|
||||
}
|
||||
@@ -689,7 +554,7 @@ impl RunningState {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
|
||||
&self.module_list
|
||||
&self._module_list
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -925,7 +790,7 @@ impl RunningState {
|
||||
variable_list: &Entity<VariableList>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
breakpoints: Entity<BreakpointList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, RunningState>,
|
||||
@@ -949,7 +814,7 @@ impl RunningState {
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
breakpoints.focus_handle(cx),
|
||||
breakpoints.clone().into(),
|
||||
breakpoints.into(),
|
||||
DebuggerPaneItem::BreakpointList,
|
||||
None,
|
||||
cx,
|
||||
|
||||
@@ -3,7 +3,7 @@ use project::debugger::session::{Session, SessionEvent};
|
||||
use ui::prelude::*;
|
||||
use util::maybe;
|
||||
|
||||
pub(crate) struct LoadedSourceList {
|
||||
pub struct LoadedSourceList {
|
||||
list: ListState,
|
||||
invalidate: bool,
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
@@ -194,10 +194,6 @@ impl VariableList {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn has_open_context_menu(&self) -> bool {
|
||||
self.open_context_menu.is_some()
|
||||
}
|
||||
|
||||
fn build_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||
return;
|
||||
|
||||
@@ -46,7 +46,7 @@ use workspace::{
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
pub(crate) struct IncludeWarnings(bool);
|
||||
struct IncludeWarnings(bool);
|
||||
impl Global for IncludeWarnings {}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -209,7 +209,6 @@ impl ProjectDiagnosticsEditor {
|
||||
.detach();
|
||||
cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
|
||||
this.include_warnings = cx.global::<IncludeWarnings>().0;
|
||||
this.diagnostics.clear();
|
||||
this.update_all_excerpts(window, cx);
|
||||
})
|
||||
.detach();
|
||||
@@ -301,8 +300,11 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_warnings(&mut self, _: &ToggleWarnings, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.set_global(IncludeWarnings(!self.include_warnings));
|
||||
fn toggle_warnings(&mut self, _: &ToggleWarnings, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.include_warnings = !self.include_warnings;
|
||||
cx.set_global(IncludeWarnings(self.include_warnings));
|
||||
self.update_all_excerpts(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -379,6 +381,7 @@ impl ProjectDiagnosticsEditor {
|
||||
Point::zero()..buffer_snapshot.max_point(),
|
||||
false,
|
||||
)
|
||||
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
|
||||
.collect::<Vec<_>>();
|
||||
let unchanged = this.update(cx, |this, _| {
|
||||
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
||||
@@ -479,10 +482,7 @@ impl ProjectDiagnosticsEditor {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_anchor_ranges([range_to_select]);
|
||||
})
|
||||
});
|
||||
if this.focus_handle.is_focused(window) {
|
||||
this.editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use language::Diagnostic;
|
||||
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
|
||||
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
|
||||
|
||||
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
|
||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
@@ -94,11 +94,6 @@ impl Render for DiagnosticIndicator {
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
if this.summary.error_count == 0 && this.summary.warning_count > 0 {
|
||||
cx.update_global(|show_warnings: &mut IncludeWarnings, _| {
|
||||
show_warnings.0 = true
|
||||
});
|
||||
}
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(
|
||||
workspace,
|
||||
|
||||
@@ -306,8 +306,6 @@ actions!(
|
||||
GoToPreviousHunk,
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
GoToNextChange,
|
||||
GoToPreviousChange,
|
||||
GoToPreviousDiagnostic,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
|
||||
@@ -49,8 +49,8 @@ use language::{
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
|
||||
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
|
||||
RowInfo, ToOffset, ToPoint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -574,21 +574,6 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
|
||||
let to_remove = self
|
||||
.inlay_map
|
||||
.current_inlays()
|
||||
.filter_map(|inlay| {
|
||||
if excerpts_removed.contains(&inlay.position.excerpt_id) {
|
||||
Some(inlay.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.inlay_map.splice(&to_remove, Vec::new());
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
|
||||
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
|
||||
let language = buffer
|
||||
|
||||
@@ -693,52 +693,6 @@ pub trait Addon: 'static {
|
||||
fn to_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
/// A set of caret positions, registered when the editor was edited.
|
||||
pub struct ChangeList {
|
||||
changes: Vec<Vec<Anchor>>,
|
||||
/// Currently "selected" change.
|
||||
position: Option<usize>,
|
||||
}
|
||||
|
||||
impl ChangeList {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
changes: Vec::new(),
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change.
|
||||
/// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction.
|
||||
pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> {
|
||||
if self.changes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev = self.position.unwrap_or(self.changes.len());
|
||||
let next = if direction == Direction::Prev {
|
||||
prev.saturating_sub(count)
|
||||
} else {
|
||||
(prev + count).min(self.changes.len() - 1)
|
||||
};
|
||||
self.position = Some(next);
|
||||
self.changes.get(next).map(|anchors| anchors.as_slice())
|
||||
}
|
||||
|
||||
/// Adds a new change to the list, resetting the change list position.
|
||||
pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec<Anchor>) {
|
||||
self.position.take();
|
||||
if pop_state {
|
||||
self.changes.pop();
|
||||
}
|
||||
self.changes.push(new_positions.clone());
|
||||
}
|
||||
|
||||
pub fn last(&self) -> Option<&[Anchor]> {
|
||||
self.changes.last().map(|anchors| anchors.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -903,7 +857,6 @@ pub struct Editor {
|
||||
serialize_folds: Task<()>,
|
||||
mouse_cursor_hidden: bool,
|
||||
hide_mouse_mode: HideMouseMode,
|
||||
pub change_list: ChangeList,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -1695,7 +1648,6 @@ impl Editor {
|
||||
hide_mouse_mode: EditorSettings::get_global(cx)
|
||||
.hide_mouse
|
||||
.unwrap_or_default(),
|
||||
change_list: ChangeList::new(),
|
||||
};
|
||||
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
|
||||
this._subscriptions
|
||||
@@ -1709,8 +1661,8 @@ impl Editor {
|
||||
this._subscriptions.push(cx.subscribe_in(
|
||||
&cx.entity(),
|
||||
window,
|
||||
|editor, _, e: &EditorEvent, window, cx| match e {
|
||||
EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
|editor, _, e: &EditorEvent, window, cx| {
|
||||
if let EditorEvent::SelectionsChanged { local } = e {
|
||||
if *local {
|
||||
let new_anchor = editor.scroll_manager.anchor();
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
@@ -1722,30 +1674,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
if !vim_enabled(cx) {
|
||||
let (map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
let pop_state = editor
|
||||
.change_list
|
||||
.last()
|
||||
.map(|previous| {
|
||||
previous.len() == selections.len()
|
||||
&& previous.iter().enumerate().all(|(ix, p)| {
|
||||
p.to_display_point(&map).row()
|
||||
== selections[ix].head().row()
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let new_positions = selections
|
||||
.into_iter()
|
||||
.map(|s| map.display_point_to_anchor(s.head(), Bias::Left))
|
||||
.collect();
|
||||
editor
|
||||
.change_list
|
||||
.push_to_change_list(pop_state, new_positions);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -4242,13 +4170,10 @@ impl Editor {
|
||||
if let Some(InlaySplice {
|
||||
to_remove,
|
||||
to_insert,
|
||||
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
|
||||
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
|
||||
{
|
||||
self.splice_inlays(&to_remove, to_insert, cx);
|
||||
}
|
||||
self.display_map.update(cx, |display_map, _| {
|
||||
display_map.remove_inlays_for_excerpts(&excerpts_removed)
|
||||
});
|
||||
return;
|
||||
}
|
||||
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
@@ -4812,8 +4737,8 @@ impl Editor {
|
||||
let lookahead = replace_range
|
||||
.end
|
||||
.saturating_sub(newest_anchor.end.text_anchor.to_offset(buffer));
|
||||
let prefix = &old_text[..old_text.len().saturating_sub(lookahead)];
|
||||
let suffix = &old_text[lookbehind.min(old_text.len())..];
|
||||
let prefix = &old_text[..old_text.len() - lookahead];
|
||||
let suffix = &old_text[lookbehind..];
|
||||
|
||||
let selections = self.selections.all::<usize>(cx);
|
||||
let mut edits = Vec::new();
|
||||
@@ -4828,7 +4753,7 @@ impl Editor {
|
||||
|
||||
// if prefix is present, don't duplicate it
|
||||
if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) {
|
||||
text = &new_text[lookbehind.min(new_text.len())..];
|
||||
text = &new_text[lookbehind..];
|
||||
|
||||
// if suffix is also present, mimic the newest cursor and replace it
|
||||
if selection.id != newest_anchor.id
|
||||
@@ -12594,45 +12519,6 @@ impl Editor {
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let old_range = selection.start..selection.end;
|
||||
|
||||
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
|
||||
// manually select word at selection
|
||||
if ["string_content", "inline"].contains(&node.kind()) {
|
||||
let word_range = {
|
||||
let display_point = buffer
|
||||
.offset_to_point(old_range.start)
|
||||
.to_display_point(&display_map);
|
||||
let Range { start, end } =
|
||||
movement::surrounding_word(&display_map, display_point);
|
||||
start.to_point(&display_map).to_offset(&buffer)
|
||||
..end.to_point(&display_map).to_offset(&buffer)
|
||||
};
|
||||
// ignore if word is already selected
|
||||
if !word_range.is_empty() && old_range != word_range {
|
||||
let last_word_range = {
|
||||
let display_point = buffer
|
||||
.offset_to_point(old_range.end)
|
||||
.to_display_point(&display_map);
|
||||
let Range { start, end } =
|
||||
movement::surrounding_word(&display_map, display_point);
|
||||
start.to_point(&display_map).to_offset(&buffer)
|
||||
..end.to_point(&display_map).to_offset(&buffer)
|
||||
};
|
||||
// only select word if start and end point belongs to same word
|
||||
if word_range == last_word_range {
|
||||
selected_larger_node = true;
|
||||
return Selection {
|
||||
id: selection.id,
|
||||
start: word_range.start,
|
||||
end: word_range.end,
|
||||
goal: SelectionGoal::None,
|
||||
reversed: selection.reversed,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_range = old_range.clone();
|
||||
let mut new_node = None;
|
||||
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
|
||||
@@ -13375,48 +13261,6 @@ impl Editor {
|
||||
.or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
|
||||
}
|
||||
|
||||
fn go_to_next_change(
|
||||
&mut self,
|
||||
_: &GoToNextChange,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(selections) = self
|
||||
.change_list
|
||||
.next_change(1, Direction::Next)
|
||||
.map(|s| s.to_vec())
|
||||
{
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let map = s.display_map();
|
||||
s.select_display_ranges(selections.iter().map(|a| {
|
||||
let point = a.to_display_point(&map);
|
||||
point..point
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_previous_change(
|
||||
&mut self,
|
||||
_: &GoToPreviousChange,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(selections) = self
|
||||
.change_list
|
||||
.next_change(1, Direction::Prev)
|
||||
.map(|s| s.to_vec())
|
||||
{
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let map = s.display_map();
|
||||
s.select_display_ranges(selections.iter().map(|a| {
|
||||
let point = a.to_display_point(&map);
|
||||
point..point
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
position: Anchor,
|
||||
@@ -13879,6 +13723,8 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<Navigated>>> {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
|
||||
|
||||
let selection = self.selections.newest::<usize>(cx);
|
||||
let multi_buffer = self.buffer.read(cx);
|
||||
let head = selection.head();
|
||||
@@ -17825,7 +17671,11 @@ impl Editor {
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = vim_enabled(cx);
|
||||
let vim_mode = cx
|
||||
.global::<SettingsStore>()
|
||||
.raw_user_settings()
|
||||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true));
|
||||
|
||||
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
|
||||
let copilot_enabled = edit_predictions_provider
|
||||
@@ -18271,13 +18121,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn vim_enabled(cx: &App) -> bool {
|
||||
cx.global::<SettingsStore>()
|
||||
.raw_user_settings()
|
||||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true))
|
||||
}
|
||||
|
||||
// Consider user intent and default settings
|
||||
fn choose_completion_range(
|
||||
completion: &Completion,
|
||||
|
||||
@@ -6309,187 +6309,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
let var1 = "«ˇtext»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
|
||||
let text = r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "hello world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
|
||||
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
// Test 1: Cursor on a letter of a string word
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "hˇello world";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "«ˇhello» world";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Test 2: Partial selection within a word
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "h«elˇ»lo world";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "«ˇhello» world";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Test 3: Complete word already selected
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "«helloˇ» world";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "«hello worldˇ»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Test 4: Selection spanning across words
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "hel«lo woˇ»rld";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "«ˇhello world»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Test 5: Expansion beyond string
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
|
||||
fn fn_1(param1: bool, param2: &str) {
|
||||
«ˇlet var1 = "hello world";»
|
||||
«ˇlet var1 = "text";»
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
|
||||
@@ -435,8 +435,6 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::stage_and_next);
|
||||
register_action(editor, window, Editor::unstage_and_next);
|
||||
register_action(editor, window, Editor::expand_all_diff_hunks);
|
||||
register_action(editor, window, Editor::go_to_previous_change);
|
||||
register_action(editor, window, Editor::go_to_next_change);
|
||||
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
if let Some(task) = editor.format(action, window, cx) {
|
||||
|
||||
@@ -555,12 +555,12 @@ impl InlayHintCache {
|
||||
/// Completely forget of certain excerpts that were removed from the multibuffer.
|
||||
pub(super) fn remove_excerpts(
|
||||
&mut self,
|
||||
excerpts_removed: &[ExcerptId],
|
||||
excerpts_removed: Vec<ExcerptId>,
|
||||
) -> Option<InlaySplice> {
|
||||
let mut to_remove = Vec::new();
|
||||
for excerpt_to_remove in excerpts_removed {
|
||||
self.update_tasks.remove(excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
|
||||
self.update_tasks.remove(&excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
|
||||
let cached_hints = cached_hints.read();
|
||||
to_remove.extend(cached_hints.ordered_hints.iter().copied());
|
||||
}
|
||||
@@ -989,16 +989,6 @@ fn fetch_and_update_hints(
|
||||
}
|
||||
|
||||
let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
|
||||
if !editor.registered_buffers.contains_key(&query.buffer_id) {
|
||||
if let Some(project) = editor.project.as_ref() {
|
||||
project.update(cx, |project, cx| {
|
||||
editor.registered_buffers.insert(
|
||||
query.buffer_id,
|
||||
project.register_buffer_with_language_servers(&buffer, cx),
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
editor
|
||||
.semantics_provider
|
||||
.as_ref()?
|
||||
|
||||
@@ -16,7 +16,6 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
dirs = "5.0"
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -38,11 +37,9 @@ reqwest_client.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
telemetry.workspace = true
|
||||
toml.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
mod example;
|
||||
mod ids;
|
||||
|
||||
use client::{Client, ProxySettings, UserStore};
|
||||
pub(crate) use example::*;
|
||||
use telemetry;
|
||||
|
||||
use ::fs::RealFs;
|
||||
use anyhow::{Result, anyhow};
|
||||
use clap::Parser;
|
||||
use extension::ExtensionHostProxy;
|
||||
use futures::future;
|
||||
use futures::stream::StreamExt;
|
||||
use gpui::http_client::{Uri, read_proxy_from_env};
|
||||
use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, Task, UpdateGlobal};
|
||||
use gpui_tokio::Tokio;
|
||||
@@ -42,18 +39,9 @@ struct Args {
|
||||
/// Model to use (default: "claude-3-7-sonnet-latest")
|
||||
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
|
||||
model: String,
|
||||
/// Languages to run (comma-separated, e.g. "js,ts,py"). If unspecified, only Rust examples are run.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
languages: Option<Vec<String>>,
|
||||
/// How many times to run each example. Note that this is currently not very efficient as N
|
||||
/// worktrees will be created for the examples.
|
||||
#[arg(long, default_value = "1")]
|
||||
repetitions: u32,
|
||||
/// How many times to run the judge on each example run.
|
||||
#[arg(long, default_value = "3")]
|
||||
judge_repetitions: u32,
|
||||
/// Maximum number of examples to run concurrently.
|
||||
#[arg(long, default_value = "10")]
|
||||
concurrency: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -86,15 +74,6 @@ fn main() {
|
||||
app.run(move |cx| {
|
||||
let app_state = init(cx);
|
||||
|
||||
let system_id = ids::get_or_create_id(&ids::eval_system_id_path()).ok();
|
||||
let installation_id = ids::get_or_create_id(&ids::eval_installation_id_path()).ok();
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
app_state
|
||||
.client
|
||||
.telemetry()
|
||||
.start(system_id, installation_id, session_id, cx);
|
||||
|
||||
let model = find_model("claude-3-7-sonnet-latest", cx).unwrap();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
@@ -150,20 +129,12 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: This creates a worktree per repetition. Ideally these examples should
|
||||
// either be run sequentially on the same worktree, or reuse worktrees when there
|
||||
// are more examples to run than the concurrency limit.
|
||||
for repetition_number in 0..args.repetitions {
|
||||
let mut example = example.clone();
|
||||
example.set_repetition_number(repetition_number);
|
||||
|
||||
let name_len = example.name.len();
|
||||
if name_len > max_name_width {
|
||||
max_name_width = example.name.len();
|
||||
}
|
||||
|
||||
examples.push(example);
|
||||
let name_len = example.name.len();
|
||||
if name_len > max_name_width {
|
||||
max_name_width = example.name.len();
|
||||
}
|
||||
|
||||
examples.push(example);
|
||||
}
|
||||
|
||||
println!("Skipped examples: {}\n", skipped.join(", "));
|
||||
@@ -232,26 +203,18 @@ fn main() {
|
||||
example.setup().await?;
|
||||
}
|
||||
|
||||
let judge_repetitions = args.judge_repetitions;
|
||||
let concurrency = args.concurrency;
|
||||
|
||||
let tasks = examples
|
||||
.into_iter()
|
||||
.map(|example| {
|
||||
let app_state = app_state.clone();
|
||||
let model = model.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result =
|
||||
run_example(&example, model, app_state, judge_repetitions, cx).await;
|
||||
(result, example)
|
||||
(run_example(&example, model, app_state, cx).await, example)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let results = futures::stream::iter(tasks)
|
||||
.buffer_unordered(concurrency)
|
||||
.collect::<Vec<(Result<Vec<Result<JudgeOutput>>>, Example)>>()
|
||||
.await;
|
||||
let results: Vec<(Result<JudgeOutput>, Example)> = future::join_all(tasks).await;
|
||||
|
||||
println!("\n\n");
|
||||
println!("========================================");
|
||||
@@ -266,25 +229,16 @@ fn main() {
|
||||
Err(err) => {
|
||||
println!("💥 {}{:?}", example.log_prefix, err);
|
||||
}
|
||||
Ok(judge_results) => {
|
||||
for judge_result in judge_results {
|
||||
match judge_result {
|
||||
Ok(judge_output) => {
|
||||
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
|
||||
let score: u32 = judge_output.score;
|
||||
let score_index = (score.min(5)) as usize;
|
||||
Ok(judge_output) => {
|
||||
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
|
||||
|
||||
println!(
|
||||
"{} {}{}",
|
||||
SCORES[score_index], example.log_prefix, judge_output.score,
|
||||
);
|
||||
judge_scores.push(judge_output.score);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("💥 {}{:?}", example.log_prefix, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{} {}{}",
|
||||
SCORES[judge_output.score.min(5) as usize],
|
||||
example.log_prefix,
|
||||
judge_output.score,
|
||||
);
|
||||
judge_scores.push(judge_output.score);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
@@ -302,10 +256,6 @@ fn main() {
|
||||
/ (score_count as f32);
|
||||
println!("\nAverage score: {average_score}");
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
app_state.client.telemetry().flush_events();
|
||||
|
||||
cx.update(|cx| cx.quit())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
@@ -316,55 +266,12 @@ async fn run_example(
|
||||
example: &Example,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
app_state: Arc<AgentAppState>,
|
||||
judge_repetitions: u32,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Vec<Result<JudgeOutput>>> {
|
||||
let run_output = cx
|
||||
.update(|cx| example.run(model.clone(), app_state.clone(), cx))?
|
||||
) -> Result<JudgeOutput> {
|
||||
cx.update(|cx| example.run(model.clone(), app_state, cx))?
|
||||
.await?;
|
||||
let diff = example.repository_diff().await?;
|
||||
|
||||
// Run judge for each repetition
|
||||
let mut results = Vec::new();
|
||||
for round in 0..judge_repetitions {
|
||||
let judge_result = example.judge(model.clone(), diff.clone(), round, cx).await;
|
||||
|
||||
if let Ok(judge_output) = &judge_result {
|
||||
let cohort_id = example
|
||||
.output_file_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string());
|
||||
|
||||
let path = std::path::Path::new(".");
|
||||
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Eval Completed",
|
||||
cohort_id = cohort_id,
|
||||
example_name = example.name.clone(),
|
||||
round = round,
|
||||
score = judge_output.score,
|
||||
analysis = judge_output.analysis,
|
||||
tool_use_counts = run_output.tool_use_counts,
|
||||
response_count = run_output.response_count,
|
||||
token_usage = run_output.token_usage,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
repository_url = example.base.url.clone(),
|
||||
repository_revision = example.base.revision.clone(),
|
||||
diagnostics_summary = run_output.diagnostics,
|
||||
commit_id = commit_id
|
||||
);
|
||||
}
|
||||
|
||||
results.push(judge_result);
|
||||
}
|
||||
|
||||
app_state.client.telemetry().flush_events();
|
||||
|
||||
Ok(results)
|
||||
example.judge(model, diff, cx).await
|
||||
}
|
||||
|
||||
fn list_all_examples() -> Result<Vec<PathBuf>> {
|
||||
@@ -526,13 +433,3 @@ pub fn authenticate_model_provider(
|
||||
let model_provider = model_registry.provider(&provider_id).unwrap();
|
||||
model_provider.authenticate(cx)
|
||||
}
|
||||
|
||||
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
|
||||
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
|
||||
}
|
||||
|
||||
pub fn get_current_commit_id_sync(repo_path: &Path) -> String {
|
||||
futures::executor::block_on(async {
|
||||
get_current_commit_id(repo_path).await.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,8 +58,6 @@ pub struct Example {
|
||||
pub criteria: String,
|
||||
/// Markdown output file to append to
|
||||
pub output_file: Option<Arc<Mutex<File>>>,
|
||||
/// Path to the output run directory.
|
||||
pub run_dir: PathBuf,
|
||||
/// Path to markdown output file
|
||||
pub output_file_path: PathBuf,
|
||||
/// Prefix used for logging that identifies this example
|
||||
@@ -94,27 +92,23 @@ impl Example {
|
||||
let base_path = dir_path.join("base.toml");
|
||||
let prompt_path = dir_path.join("prompt.md");
|
||||
let criteria_path = dir_path.join("criteria.md");
|
||||
let output_file_path = run_dir.join(format!("{}.md", name));
|
||||
|
||||
let output_file_path = run_dir.join(format!(
|
||||
"{}.md",
|
||||
dir_path.file_name().unwrap().to_str().unwrap()
|
||||
));
|
||||
|
||||
Ok(Example {
|
||||
name: name.clone(),
|
||||
base: toml::from_str(&fs::read_to_string(&base_path)?)?,
|
||||
prompt: fs::read_to_string(prompt_path.clone())?,
|
||||
criteria: fs::read_to_string(criteria_path.clone())?,
|
||||
run_dir: run_dir.to_path_buf(),
|
||||
output_file: None,
|
||||
output_file_path,
|
||||
log_prefix: name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_repetition_number(&mut self, repetition_number: u32) {
|
||||
if repetition_number > 0 {
|
||||
self.name = format!("{}-{}", self.name, repetition_number);
|
||||
self.output_file_path = self.run_dir.join(format!("{}.md", self.name));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_log_prefix_style(&mut self, color: &str, name_width: usize) {
|
||||
self.log_prefix = format!(
|
||||
"{}{:<width$}\x1b[0m | ",
|
||||
@@ -140,21 +134,13 @@ impl Example {
|
||||
pub async fn setup(&mut self) -> Result<()> {
|
||||
let repo_path = repo_path_for_url(&self.base.url);
|
||||
|
||||
let revision_exists = run_git(&repo_path, &["rev-parse", "--verify", &self.base.revision])
|
||||
.await
|
||||
.is_ok();
|
||||
println!("{}Fetching", self.log_prefix);
|
||||
|
||||
if !revision_exists {
|
||||
println!(
|
||||
"{}Fetching revision {}",
|
||||
self.log_prefix, &self.base.revision
|
||||
);
|
||||
run_git(
|
||||
&repo_path,
|
||||
&["fetch", "--depth", "1", "origin", &self.base.revision],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
run_git(
|
||||
&repo_path,
|
||||
&["fetch", "--depth", "1", "origin", &self.base.revision],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let worktree_path = self.worktree_path();
|
||||
|
||||
@@ -386,26 +372,18 @@ impl Example {
|
||||
pending_tool_use,
|
||||
..
|
||||
} => {
|
||||
if let Some(tool_use) = pending_tool_use {
|
||||
let message = format!("TOOL FINISHED: {}", tool_use.name);
|
||||
println!("{}{message}", log_prefix);
|
||||
writeln!(&mut output_file, "\n{}", message).log_err();
|
||||
}
|
||||
thread.update(cx, |thread, _cx| {
|
||||
if let Some(tool_use) = pending_tool_use {
|
||||
if let Some(tool_result) = thread.tool_result(&tool_use_id) {
|
||||
let message = if tool_result.is_error {
|
||||
format!("TOOL FAILED: {}", tool_use.name)
|
||||
} else {
|
||||
format!("TOOL FINISHED: {}", tool_use.name)
|
||||
};
|
||||
println!("{log_prefix}{message}");
|
||||
writeln!(&mut output_file, "\n{}", message).log_err();
|
||||
writeln!(&mut output_file, "\n{}\n", tool_result.content).log_err();
|
||||
let mut tool_use_counts = tool_use_counts.lock().unwrap();
|
||||
*tool_use_counts
|
||||
.entry(tool_result.tool_name.clone())
|
||||
.or_insert(0) += 1;
|
||||
} else {
|
||||
let message = format!("TOOL FINISHED WITHOUT RESULT: {}", tool_use.name);
|
||||
println!("{log_prefix}{message}");
|
||||
writeln!(&mut output_file, "\n{}", message).log_err();
|
||||
}
|
||||
if let Some(tool_result) = thread.tool_result(&tool_use_id) {
|
||||
writeln!(&mut output_file, "\n{}\n", tool_result.content).log_err();
|
||||
let mut tool_use_counts = tool_use_counts.lock().unwrap();
|
||||
*tool_use_counts
|
||||
.entry(tool_result.tool_name.clone())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
})?;
|
||||
}
|
||||
@@ -447,10 +425,6 @@ impl Example {
|
||||
println!("{}Getting repository diff", this.log_prefix);
|
||||
let repository_diff = this.repository_diff().await?;
|
||||
|
||||
let repository_diff_path = this.run_dir.join(format!("{}.diff", this.name));
|
||||
let mut repository_diff_output_file = File::create(&repository_diff_path)?;
|
||||
writeln!(&mut repository_diff_output_file, "{}", &repository_diff).log_err();
|
||||
|
||||
println!("{}Getting diagnostics", this.log_prefix);
|
||||
let diagnostics = cx
|
||||
.update(move |cx| {
|
||||
@@ -482,7 +456,6 @@ impl Example {
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
repository_diff: String,
|
||||
judge_repetitions: u32,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<JudgeOutput> {
|
||||
let judge_prompt = include_str!("judge_prompt.hbs");
|
||||
@@ -510,14 +483,14 @@ impl Example {
|
||||
|
||||
let response = send_language_model_request(model, request, cx).await?;
|
||||
|
||||
let judge_file_path = self.run_dir.join(format!(
|
||||
"{}_judge_{}.md",
|
||||
self.name, // This is the eval_name
|
||||
judge_repetitions
|
||||
));
|
||||
let output_file_ref = self.output_file();
|
||||
let mut output_file = output_file_ref.lock().unwrap();
|
||||
|
||||
let mut judge_output_file = File::create(&judge_file_path)?;
|
||||
writeln!(&mut judge_output_file, "{}", &response).log_err();
|
||||
writeln!(&mut output_file, "\n\n").log_err();
|
||||
writeln!(&mut output_file, "========================================").log_err();
|
||||
writeln!(&mut output_file, " JUDGE OUTPUT ").log_err();
|
||||
writeln!(&mut output_file, "========================================").log_err();
|
||||
writeln!(&mut output_file, "\n{}", &response).log_err();
|
||||
|
||||
parse_judge_output(&response)
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn get_or_create_id(path: &Path) -> Result<String> {
|
||||
if let Ok(id) = fs::read_to_string(path) {
|
||||
let trimmed = id.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
let new_id = Uuid::new_v4().to_string();
|
||||
fs::write(path, &new_id)?;
|
||||
Ok(new_id)
|
||||
}
|
||||
|
||||
pub fn eval_system_id_path() -> PathBuf {
|
||||
dirs::data_local_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("zed-eval-system-id")
|
||||
}
|
||||
|
||||
pub fn eval_installation_id_path() -> PathBuf {
|
||||
dirs::data_local_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("zed-eval-installation-id")
|
||||
}
|
||||
@@ -84,11 +84,6 @@ impl FeatureFlag for ZedPro {
|
||||
const NAME: &'static str = "zed-pro";
|
||||
}
|
||||
|
||||
pub struct ZedProWebSearchTool {}
|
||||
impl FeatureFlag for ZedProWebSearchTool {
|
||||
const NAME: &'static str = "zed-pro-web-search-tool";
|
||||
}
|
||||
|
||||
pub struct NotebookFeatureFlag;
|
||||
|
||||
impl FeatureFlag for NotebookFeatureFlag {
|
||||
|
||||
@@ -2,7 +2,6 @@ use gpui::{App, ClipboardItem, PromptLevel, actions};
|
||||
use system_specs::SystemSpecs;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::feedback::FileBugReport;
|
||||
|
||||
pub mod feedback_modal;
|
||||
|
||||
@@ -13,6 +12,7 @@ actions!(
|
||||
[
|
||||
CopySystemSpecsIntoClipboard,
|
||||
EmailZed,
|
||||
FileBugReport,
|
||||
OpenZedRepo,
|
||||
RequestFeature,
|
||||
]
|
||||
@@ -27,7 +27,7 @@ fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
concat!(
|
||||
"https://github.com/zed-industries/zed/issues/new",
|
||||
"?",
|
||||
"template=10_bug_report.yml",
|
||||
"template=1_bug_report.yml",
|
||||
"&",
|
||||
"environment={}"
|
||||
),
|
||||
|
||||
@@ -1333,23 +1333,13 @@ impl FakeFs {
|
||||
let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else {
|
||||
anyhow::bail!("pointed-to git dir {path:?} not found")
|
||||
};
|
||||
let FakeFsEntry::Dir {
|
||||
git_repo_state,
|
||||
entries,
|
||||
..
|
||||
} = &mut *git_dir_entry.lock()
|
||||
else {
|
||||
let FakeFsEntry::Dir { git_repo_state, .. } = &mut *git_dir_entry.lock() else {
|
||||
anyhow::bail!("gitfile points to a non-directory")
|
||||
};
|
||||
let common_dir = if let Some(child) = entries.get("commondir") {
|
||||
Path::new(
|
||||
std::str::from_utf8(child.lock().file_content("commondir".as_ref())?)
|
||||
.context("commondir content")?,
|
||||
)
|
||||
.to_owned()
|
||||
} else {
|
||||
canonical_path.clone()
|
||||
};
|
||||
let common_dir = canonical_path
|
||||
.ancestors()
|
||||
.find(|ancestor| ancestor.ends_with(".git"))
|
||||
.ok_or_else(|| anyhow!("repository dir not contained in any .git"))?;
|
||||
let repo_state = git_repo_state.get_or_insert_with(|| {
|
||||
Arc::new(Mutex::new(FakeGitRepositoryState::new(
|
||||
state.git_event_tx.clone(),
|
||||
@@ -1357,7 +1347,7 @@ impl FakeFs {
|
||||
});
|
||||
let mut repo_state = repo_state.lock();
|
||||
|
||||
let result = f(&mut repo_state, &canonical_path, &common_dir);
|
||||
let result = f(&mut repo_state, &canonical_path, common_dir);
|
||||
|
||||
if emit_git_event {
|
||||
state.emit_event([(canonical_path, None)]);
|
||||
|
||||
@@ -1013,6 +1013,7 @@ impl GitRepository for RealGitRepository {
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory)
|
||||
.args(["push"])
|
||||
.args(options.map(|option| match option {
|
||||
@@ -1044,6 +1045,7 @@ impl GitRepository for RealGitRepository {
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory?)
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
@@ -1068,6 +1070,7 @@ impl GitRepository for RealGitRepository {
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory?)
|
||||
.args(["fetch", "--all"])
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
|
||||
@@ -599,11 +599,33 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
|
||||
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
|
||||
where
|
||||
F: Fn(usize) -> std::cmp::Ordering,
|
||||
{
|
||||
while low < high {
|
||||
let mid = low + (high - low) / 2;
|
||||
match is_target(mid) {
|
||||
std::cmp::Ordering::Equal => return Some(mid),
|
||||
std::cmp::Ordering::Less => low = mid + 1,
|
||||
std::cmp::Ordering::Greater => high = mid,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
if self.conflicted_count > 0 {
|
||||
let conflicted_start = 1;
|
||||
if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
{
|
||||
if let Some(ix) = binary_search(
|
||||
conflicted_start,
|
||||
conflicted_start + self.conflicted_count,
|
||||
|ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
},
|
||||
) {
|
||||
return Some(ix);
|
||||
}
|
||||
}
|
||||
@@ -613,8 +635,14 @@ impl GitPanel {
|
||||
} else {
|
||||
0
|
||||
} + 1;
|
||||
if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
if let Some(ix) =
|
||||
binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
})
|
||||
{
|
||||
return Some(ix);
|
||||
}
|
||||
@@ -629,8 +657,14 @@ impl GitPanel {
|
||||
} else {
|
||||
0
|
||||
} + 1;
|
||||
if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
if let Some(ix) =
|
||||
binary_search(untracked_start, untracked_start + self.new_count, |ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
})
|
||||
{
|
||||
return Some(ix);
|
||||
}
|
||||
@@ -3577,15 +3611,6 @@ impl GitPanel {
|
||||
items
|
||||
}
|
||||
})
|
||||
.when(
|
||||
!self.horizontal_scrollbar.show_track
|
||||
&& self.horizontal_scrollbar.show_scrollbar,
|
||||
|this| {
|
||||
// when not showing the horizontal scrollbar track, make sure we don't
|
||||
// obscure the last entry
|
||||
this.pb(scroll_track_size)
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||
|
||||
@@ -589,6 +589,11 @@ impl<V> Entity<V> {
|
||||
use postage::prelude::{Sink as _, Stream as _};
|
||||
|
||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||
let timeout_duration = if cfg!(target_os = "macos") {
|
||||
Duration::from_millis(100)
|
||||
} else {
|
||||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
let mut cx = cx.app.borrow_mut();
|
||||
let subscriptions = (
|
||||
@@ -610,7 +615,7 @@ impl<V> Entity<V> {
|
||||
let handle = self.downgrade();
|
||||
|
||||
async move {
|
||||
crate::util::timeout(Duration::from_secs(1), async move {
|
||||
crate::util::timeout(timeout_duration, async move {
|
||||
loop {
|
||||
{
|
||||
let cx = cx.borrow();
|
||||
|
||||
@@ -27,8 +27,6 @@ use objc::{
|
||||
};
|
||||
use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
|
||||
|
||||
use super::NSStringExt;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MacScreenCaptureSource {
|
||||
sc_display: id,
|
||||
@@ -186,10 +184,7 @@ pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptur
|
||||
Ok(result)
|
||||
} else {
|
||||
let msg: id = msg_send![error, localizedDescription];
|
||||
Err(anyhow!(
|
||||
"Screen share failed: {:?}",
|
||||
NSStringExt::to_str(&msg)
|
||||
))
|
||||
Err(anyhow!("Failed to register: {:?}", msg))
|
||||
};
|
||||
tx.send(result).ok();
|
||||
});
|
||||
|
||||
@@ -4468,33 +4468,36 @@ impl<'a> Iterator for BufferChunks<'a> {
|
||||
}
|
||||
self.diagnostic_endpoints = diagnostic_endpoints;
|
||||
|
||||
let chunk = self.chunks.peek()?;
|
||||
|
||||
let chunk_start = self.range.start;
|
||||
let mut chunk_end = (self.chunks.offset() + chunk.len())
|
||||
.min(next_capture_start)
|
||||
.min(next_diagnostic_endpoint);
|
||||
let mut highlight_id = None;
|
||||
if let Some(highlights) = self.highlights.as_ref() {
|
||||
if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() {
|
||||
chunk_end = chunk_end.min(*parent_capture_end);
|
||||
highlight_id = Some(*parent_highlight_id);
|
||||
if let Some(chunk) = self.chunks.peek() {
|
||||
let chunk_start = self.range.start;
|
||||
let mut chunk_end = (self.chunks.offset() + chunk.len())
|
||||
.min(next_capture_start)
|
||||
.min(next_diagnostic_endpoint);
|
||||
let mut highlight_id = None;
|
||||
if let Some(highlights) = self.highlights.as_ref() {
|
||||
if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() {
|
||||
chunk_end = chunk_end.min(*parent_capture_end);
|
||||
highlight_id = Some(*parent_highlight_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let slice = &chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()];
|
||||
self.range.start = chunk_end;
|
||||
if self.range.start == self.chunks.offset() + chunk.len() {
|
||||
self.chunks.next().unwrap();
|
||||
}
|
||||
let slice =
|
||||
&chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()];
|
||||
self.range.start = chunk_end;
|
||||
if self.range.start == self.chunks.offset() + chunk.len() {
|
||||
self.chunks.next().unwrap();
|
||||
}
|
||||
|
||||
Some(Chunk {
|
||||
text: slice,
|
||||
syntax_highlight_id: highlight_id,
|
||||
diagnostic_severity: self.current_diagnostic_severity(),
|
||||
is_unnecessary: self.current_code_is_unnecessary(),
|
||||
..Default::default()
|
||||
})
|
||||
Some(Chunk {
|
||||
text: slice,
|
||||
syntax_highlight_id: highlight_id,
|
||||
diagnostic_severity: self.current_diagnostic_severity(),
|
||||
is_unnecessary: self.current_code_is_unnecessary(),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ struct SyntaxLayerEntry {
|
||||
enum SyntaxLayerContent {
|
||||
Parsed {
|
||||
tree: tree_sitter::Tree,
|
||||
brackets: SumTree<BracketItem>,
|
||||
language: Arc<Language>,
|
||||
},
|
||||
Pending {
|
||||
@@ -116,72 +115,6 @@ impl SyntaxLayerContent {
|
||||
}
|
||||
}
|
||||
|
||||
// "fn main() { }"
|
||||
// [Iso(7), Open(1), Close(1), Iso(2), Open(1), Iso(3), Close(1)]
|
||||
#[derive(Clone)]
|
||||
enum BracketItem {
|
||||
Isomorphic { len: usize },
|
||||
OpenBracket { len: usize },
|
||||
CloseBracket { len: usize },
|
||||
}
|
||||
|
||||
impl BracketItem {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
&Self::Isomorphic { len } => len,
|
||||
&Self::OpenBracket { len } => len,
|
||||
&Self::CloseBracket { len } => len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct BracketSummary {
|
||||
len: usize,
|
||||
/// The change in depth that happened inside this summary.
|
||||
depth_diff: i32,
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for BracketSummary {
|
||||
type Context = ();
|
||||
|
||||
fn zero(_: &()) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.len += summary.len;
|
||||
self.depth_diff += summary.depth_diff;
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for BracketItem {
|
||||
type Summary = BracketSummary;
|
||||
|
||||
fn summary(&self, _: &()) -> Self::Summary {
|
||||
let depth_diff = match self {
|
||||
&Self::Isomorphic { .. } => 0,
|
||||
&Self::OpenBracket { .. } => 1,
|
||||
&Self::CloseBracket { .. } => -1,
|
||||
};
|
||||
|
||||
BracketSummary {
|
||||
len: self.len(),
|
||||
depth_diff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, BracketSummary> for usize {
|
||||
fn zero(_cx: &()) -> Self {
|
||||
0
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &'a BracketSummary, _: &()) {
|
||||
*self += summary.len;
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer of syntax highlighting, corresponding to a single syntax
|
||||
/// tree in a particular language.
|
||||
#[derive(Debug)]
|
||||
@@ -515,6 +448,7 @@ impl SyntaxSnapshot {
|
||||
|
||||
let mut changed_regions = ChangeRegionSet::default();
|
||||
let mut queue = BinaryHeap::new();
|
||||
let mut combined_injection_ranges = HashMap::default();
|
||||
queue.push(ParseStep {
|
||||
depth: 0,
|
||||
language: ParseStepLanguage::Loaded {
|
||||
@@ -612,13 +546,12 @@ impl SyntaxSnapshot {
|
||||
}
|
||||
|
||||
let content = match step.language {
|
||||
ParseStepLanguage::Pending { name } => SyntaxLayerContent::Pending {
|
||||
language_name: name,
|
||||
},
|
||||
ParseStepLanguage::Loaded { language } => {
|
||||
let Some(grammar) = language.grammar() else {
|
||||
continue;
|
||||
};
|
||||
let tree;
|
||||
let changed_ranges;
|
||||
|
||||
let mut included_ranges = step.included_ranges;
|
||||
for range in &mut included_ranges {
|
||||
@@ -632,14 +565,7 @@ impl SyntaxSnapshot {
|
||||
.to_ts_point();
|
||||
}
|
||||
|
||||
let (old_tree, mut brackets) = if let Some((
|
||||
SyntaxLayerContent::Parsed {
|
||||
tree: old_tree,
|
||||
brackets,
|
||||
..
|
||||
},
|
||||
layer_start,
|
||||
)) =
|
||||
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
|
||||
old_layer.map(|layer| (&layer.content, layer.range.start))
|
||||
{
|
||||
log::trace!(
|
||||
@@ -675,7 +601,12 @@ impl SyntaxSnapshot {
|
||||
}
|
||||
|
||||
if included_ranges.is_empty() {
|
||||
included_ranges.push(zeroed_tree_sitter_range());
|
||||
included_ranges.push(tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
@@ -685,7 +616,32 @@ impl SyntaxSnapshot {
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
(Some(old_tree), brackets.clone())
|
||||
let result = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
included_ranges,
|
||||
Some(old_tree.clone()),
|
||||
);
|
||||
match result {
|
||||
Ok(t) => tree = t,
|
||||
Err(e) => {
|
||||
log::error!("error parsing text: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
changed_ranges = join_ranges(
|
||||
invalidated_ranges
|
||||
.iter()
|
||||
.filter(|&range| {
|
||||
range.start <= step_end_byte && range.end >= step_start_byte
|
||||
})
|
||||
.cloned(),
|
||||
old_tree.changed_ranges(&tree).map(|r| {
|
||||
step_start_byte + r.start_byte..step_start_byte + r.end_byte
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
if matches!(step.mode, ParseMode::Combined { .. }) {
|
||||
insert_newlines_between_ranges(
|
||||
@@ -698,7 +654,12 @@ impl SyntaxSnapshot {
|
||||
}
|
||||
|
||||
if included_ranges.is_empty() {
|
||||
included_ranges.push(zeroed_tree_sitter_range());
|
||||
included_ranges.push(tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
@@ -708,89 +669,58 @@ impl SyntaxSnapshot {
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
(None, SumTree::new(&()))
|
||||
};
|
||||
let result = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
included_ranges,
|
||||
None,
|
||||
);
|
||||
match result {
|
||||
Ok(t) => tree = t,
|
||||
Err(e) => {
|
||||
log::error!("error parsing text: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
changed_ranges = vec![step_start_byte..step_end_byte];
|
||||
}
|
||||
|
||||
let result = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
step_start_byte,
|
||||
included_ranges,
|
||||
old_tree.cloned(),
|
||||
);
|
||||
let tree = match result {
|
||||
Ok(inner) => inner,
|
||||
Err(e) => {
|
||||
log::error!("error parsing text: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let changed_ranges = if let Some(old_tree) = old_tree {
|
||||
join_ranges(
|
||||
invalidated_ranges
|
||||
.iter()
|
||||
.filter(|&range| {
|
||||
range.start <= step_end_byte && range.end >= step_start_byte
|
||||
})
|
||||
.cloned(),
|
||||
old_tree.changed_ranges(&tree).map(|r| {
|
||||
step_start_byte + r.start_byte..step_start_byte + r.end_byte
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
vec![step_start_byte..step_end_byte]
|
||||
};
|
||||
|
||||
// re-run queries if something changed
|
||||
if !changed_ranges.is_empty() {
|
||||
if let Some((config, registry)) =
|
||||
grammar.injection_config.as_ref().zip(registry.as_ref())
|
||||
{
|
||||
for range in &changed_ranges {
|
||||
let region = ChangedRegion {
|
||||
if let (Some((config, registry)), false) = (
|
||||
grammar.injection_config.as_ref().zip(registry.as_ref()),
|
||||
changed_ranges.is_empty(),
|
||||
) {
|
||||
for range in &changed_ranges {
|
||||
changed_regions.insert(
|
||||
ChangedRegion {
|
||||
depth: step.depth + 1,
|
||||
range: text.anchor_before(range.start)
|
||||
..text.anchor_after(range.end),
|
||||
};
|
||||
changed_regions.insert(region, text);
|
||||
}
|
||||
|
||||
update_injection_parse_steps(
|
||||
config,
|
||||
},
|
||||
text,
|
||||
step.range.clone(),
|
||||
tree.root_node_with_offset(
|
||||
step_start_byte,
|
||||
step_start_point.to_ts_point(),
|
||||
),
|
||||
registry,
|
||||
step.depth + 1,
|
||||
&changed_ranges,
|
||||
&mut queue,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(config) = grammar.brackets_config.as_ref() {
|
||||
update_brackets_in_range(
|
||||
config,
|
||||
text,
|
||||
tree.root_node_with_offset(
|
||||
step_start_byte,
|
||||
step_start_point.to_ts_point(),
|
||||
),
|
||||
&changed_ranges,
|
||||
&mut brackets,
|
||||
);
|
||||
}
|
||||
get_injections(
|
||||
config,
|
||||
text,
|
||||
step.range.clone(),
|
||||
tree.root_node_with_offset(
|
||||
step_start_byte,
|
||||
step_start_point.to_ts_point(),
|
||||
),
|
||||
registry,
|
||||
step.depth + 1,
|
||||
&changed_ranges,
|
||||
&mut combined_injection_ranges,
|
||||
&mut queue,
|
||||
);
|
||||
}
|
||||
|
||||
SyntaxLayerContent::Parsed {
|
||||
tree,
|
||||
language,
|
||||
brackets,
|
||||
}
|
||||
SyntaxLayerContent::Parsed { tree, language }
|
||||
}
|
||||
ParseStepLanguage::Pending { name } => SyntaxLayerContent::Pending {
|
||||
language_name: name,
|
||||
},
|
||||
};
|
||||
|
||||
layers.push(
|
||||
@@ -936,7 +866,7 @@ impl SyntaxSnapshot {
|
||||
iter::from_fn(move || {
|
||||
while let Some(layer) = cursor.item() {
|
||||
let mut info = None;
|
||||
if let SyntaxLayerContent::Parsed { tree, language, .. } = &layer.content {
|
||||
if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
|
||||
let layer_start_offset = layer.range.start.to_offset(buffer);
|
||||
let layer_start_point = layer.range.start.to_point(buffer).to_ts_point();
|
||||
if include_hidden || !language.config.hidden {
|
||||
@@ -1093,7 +1023,6 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||
pub struct TreeSitterOptions {
|
||||
max_start_depth: Option<u32>,
|
||||
}
|
||||
|
||||
impl TreeSitterOptions {
|
||||
pub fn max_start_depth(max_start_depth: u32) -> Self {
|
||||
Self {
|
||||
@@ -1321,8 +1250,7 @@ fn parse_text(
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_injection_parse_steps(
|
||||
fn get_injections(
|
||||
config: &InjectionConfig,
|
||||
text: &BufferSnapshot,
|
||||
outer_range: Range<Anchor>,
|
||||
@@ -1330,16 +1258,15 @@ fn update_injection_parse_steps(
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
depth: usize,
|
||||
changed_ranges: &[Range<usize>],
|
||||
combined_injection_ranges: &mut HashMap<LanguageId, (Arc<Language>, Vec<tree_sitter::Range>)>,
|
||||
queue: &mut BinaryHeap<ParseStep>,
|
||||
) {
|
||||
let mut query_cursor = QueryCursorHandle::new();
|
||||
let mut prev_match = None;
|
||||
|
||||
// Note: a `ParseStep` must be created for every combined injection language, even
|
||||
// if there are currently no matches for that injection.
|
||||
let mut combined_injection_ranges =
|
||||
HashMap::<LanguageId, (Arc<Language>, Vec<tree_sitter::Range>)>::default();
|
||||
|
||||
// Ensure that a `ParseStep` is created for every combined injection language, even
|
||||
// if there currently no matches for that injection.
|
||||
combined_injection_ranges.clear();
|
||||
for pattern in &config.patterns {
|
||||
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
|
||||
if let Some(language) = language_registry
|
||||
@@ -1363,6 +1290,7 @@ fn update_injection_parse_steps(
|
||||
if content_ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content_range =
|
||||
content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
|
||||
|
||||
@@ -1453,46 +1381,6 @@ fn update_injection_parse_steps(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_brackets_in_range(
|
||||
config: &BracketConfig,
|
||||
text: &BufferSnapshot,
|
||||
node: Node,
|
||||
changed_ranges: &[Range<usize>],
|
||||
brackets_tree: &mut SumTree<BracketItem>,
|
||||
) {
|
||||
let mut query_cursor = QueryCursorHandle::new();
|
||||
|
||||
for changed_range in changed_ranges {
|
||||
let mut new_bracket_subtree = SumTree::new(&());
|
||||
|
||||
// TODO: is this saturating_sub necessary?
|
||||
query_cursor.set_byte_range(changed_range.start.saturating_sub(1)..changed_range.end + 1);
|
||||
|
||||
for mat in query_cursor.matches(&config.query, node, TextProvider(text.as_rope())) {
|
||||
let open_capture = mat.nodes_for_capture_index(config.open_capture_ix).next();
|
||||
let close_capture = mat.nodes_for_capture_index(config.close_capture_ix).next();
|
||||
|
||||
let Some((open_capture, close_capture)) = open_capture.zip(close_capture) else {
|
||||
log::warn!("couldn't find @open and @close captures in brackets pattern");
|
||||
continue;
|
||||
};
|
||||
|
||||
todo!("how do I insert this into the SumTree, or, should I put it in a BTreeSet");
|
||||
// cursor.see
|
||||
}
|
||||
|
||||
let mut cursor = brackets_tree.cursor::<usize>(&());
|
||||
let mut left_side = cursor.slice(&changed_range.start, Bias::Left, &());
|
||||
cursor.seek(&changed_range.end, Bias::Right, &());
|
||||
let right_side = cursor.suffix(&());
|
||||
|
||||
left_side.append(new_bracket_subtree, &());
|
||||
left_side.append(right_side, &());
|
||||
drop(cursor); // make the borrow checker happy
|
||||
*brackets_tree = left_side;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the given list of included `ranges`, removing any ranges that intersect
|
||||
/// `removed_ranges`, and inserting the given `new_ranges`.
|
||||
///
|
||||
@@ -2015,12 +1903,3 @@ impl fmt::Debug for LogPoint {
|
||||
(self.0.row, self.0.column).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn zeroed_tree_sitter_range() -> tree_sitter::Range {
|
||||
tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +97,7 @@ pub struct TokenUsage {
|
||||
|
||||
impl TokenUsage {
|
||||
pub fn total_tokens(&self) -> u32 {
|
||||
self.input_tokens
|
||||
+ self.output_tokens
|
||||
+ self.cache_read_input_tokens
|
||||
+ self.cache_creation_input_tokens
|
||||
self.input_tokens + self.output_tokens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,27 +142,6 @@ impl fmt::Display for MaxMonthlySpendReachedError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub struct ModelRequestLimitReachedError {
|
||||
pub plan: Plan,
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelRequestLimitReachedError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let message = match self.plan {
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
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."
|
||||
}
|
||||
};
|
||||
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
|
||||
|
||||
|
||||
@@ -546,6 +546,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
||||
let plan = proto::Plan::ZedPro;
|
||||
let is_trial = false;
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
@@ -557,6 +558,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.justify_between()
|
||||
.when(cx.has_flag::<ZedPro>(), |this| {
|
||||
this.child(match plan {
|
||||
// Already a Zed Pro subscriber
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
|
||||
.icon(IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -566,9 +568,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
window
|
||||
.dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
|
||||
}),
|
||||
Plan::Free | Plan::ZedProTrial => Button::new(
|
||||
// Free user
|
||||
Plan::Free => Button::new(
|
||||
"try-pro",
|
||||
if plan == Plan::ZedProTrial {
|
||||
if is_trial {
|
||||
"Upgrade to Pro"
|
||||
} else {
|
||||
"Try Pro"
|
||||
|
||||
@@ -53,7 +53,6 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -705,12 +705,12 @@ pub fn map_to_language_model_completion_events(
|
||||
update_usage(&mut state.usage, &message.usage);
|
||||
return Some((
|
||||
vec![
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
|
||||
&state.usage,
|
||||
))),
|
||||
Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
}),
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
|
||||
&state.usage,
|
||||
))),
|
||||
],
|
||||
state,
|
||||
));
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use anthropic::{AnthropicError, AnthropicModelMode, parse_prompt_too_long};
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use client::{
|
||||
Client, EXPIRED_LLM_TOKEN_HEADER_NAME, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
PerformCompletionParams, UserStore, zed_urls,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::{FeatureFlagAppExt, LlmClosedBeta, ZedPro};
|
||||
use futures::{
|
||||
@@ -13,20 +16,18 @@ use language_model::{
|
||||
AuthenticateError, CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
|
||||
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelToolSchemaFormat, RateLimiter, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
|
||||
MaxMonthlySpendReachedError, PaymentRequiredError, RefreshLlmTokenListener,
|
||||
};
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::value::RawValue;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::Timer;
|
||||
use smol::io::{AsyncReadExt, BufReader};
|
||||
use std::str::FromStr as _;
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
@@ -34,11 +35,6 @@ use std::{
|
||||
use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{TintColor, prelude::*};
|
||||
use zed_llm_client::{
|
||||
CURRENT_PLAN_HEADER_NAME, CompletionBody, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
};
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::provider::anthropic::{count_anthropic_tokens, into_anthropic};
|
||||
@@ -517,7 +513,7 @@ impl CloudLanguageModel {
|
||||
async fn perform_llm_completion(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
body: CompletionBody,
|
||||
body: PerformCompletionParams,
|
||||
) -> Result<Response<AsyncBody>> {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
@@ -555,33 +551,6 @@ impl CloudLanguageModel {
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!(MaxMonthlySpendReachedError));
|
||||
} else if status == StatusCode::FORBIDDEN
|
||||
&& response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
if let Some(MODEL_REQUESTS_RESOURCE_HEADER_VALUE) = response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.and_then(|resource| resource.to_str().ok())
|
||||
{
|
||||
if let Some(plan) = response
|
||||
.headers()
|
||||
.get(CURRENT_PLAN_HEADER_NAME)
|
||||
.and_then(|plan| plan.to_str().ok())
|
||||
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
|
||||
{
|
||||
let plan = match plan {
|
||||
zed_llm_client::Plan::Free => Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
};
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(anyhow!("Forbidden"));
|
||||
} else if status.as_u16() >= 500 && status.as_u16() < 600 {
|
||||
// If we encounter an error in the 500 range, retry after a delay.
|
||||
// We've seen at least these in the wild from API providers:
|
||||
@@ -725,10 +694,12 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let response = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
CompletionBody {
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -764,10 +735,12 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let response = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
CompletionBody {
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -787,10 +760,12 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let response = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
CompletionBody {
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::Google,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -206,12 +206,12 @@ impl Render for KeyContextView {
|
||||
.mt_4()
|
||||
.gap_4()
|
||||
.child(
|
||||
Button::new("open_documentation", "Open Documentation")
|
||||
Button::new("default", "Open Documentation")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
|
||||
)
|
||||
.child(
|
||||
Button::new("view_default_keymap", "View default keymap")
|
||||
Button::new("default", "View default keymap")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(
|
||||
&zed_actions::OpenDefaultKeymap,
|
||||
@@ -219,14 +219,16 @@ impl Render for KeyContextView {
|
||||
cx
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
|
||||
window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("edit_your_keymap", "Edit your keymap")
|
||||
Button::new("default", "Edit your keymap")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window, cx))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
|
||||
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,17 +2,25 @@
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
|
||||
(try_statement
|
||||
body: (_) @start
|
||||
[(except_clause) (finally_clause)] @end
|
||||
) @indent
|
||||
[
|
||||
(if_statement)
|
||||
(for_statement)
|
||||
(while_statement)
|
||||
(with_statement)
|
||||
(function_definition)
|
||||
(class_definition)
|
||||
(match_statement)
|
||||
(try_statement)
|
||||
] @indent
|
||||
|
||||
(if_statement
|
||||
consequence: (_) @start
|
||||
alternative: (_) @end
|
||||
) @indent
|
||||
[
|
||||
(else_clause)
|
||||
(elif_clause)
|
||||
(except_clause)
|
||||
(finally_clause)
|
||||
] @outdent
|
||||
|
||||
(_
|
||||
alternative: (elif_clause) @start
|
||||
alternative: (_) @end
|
||||
) @indent
|
||||
[
|
||||
(block)
|
||||
(case_clause)
|
||||
] @indent
|
||||
|
||||
@@ -39,9 +39,7 @@ pub(crate) mod m_2025_03_29 {
|
||||
}
|
||||
|
||||
pub(crate) mod m_2025_04_15 {
|
||||
mod keymap;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use keymap::KEYMAP_PATTERNS;
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use std::{ops::Range, sync::LazyLock};
|
||||
use tree_sitter::{Query, QueryMatch};
|
||||
|
||||
use crate::MigrationPatterns;
|
||||
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
|
||||
|
||||
pub const KEYMAP_PATTERNS: MigrationPatterns =
|
||||
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
|
||||
|
||||
fn replace_string_action(
|
||||
contents: &str,
|
||||
mat: &QueryMatch,
|
||||
query: &Query,
|
||||
) -> Option<(Range<usize>, String)> {
|
||||
let action_name_ix = query.capture_index_for_name("action_name")?;
|
||||
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
|
||||
let action_name_range = action_name_node.byte_range();
|
||||
let action_name = contents.get(action_name_range.clone())?;
|
||||
|
||||
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
|
||||
return Some((action_name_range, new_action_name.to_string()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
|
||||
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
|
||||
HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")])
|
||||
});
|
||||
@@ -98,10 +98,6 @@ pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
|
||||
migrations::m_2025_03_06::KEYMAP_PATTERNS,
|
||||
&KEYMAP_QUERY_2025_03_06,
|
||||
),
|
||||
(
|
||||
migrations::m_2025_04_15::KEYMAP_PATTERNS,
|
||||
&KEYMAP_QUERY_2025_04_15,
|
||||
),
|
||||
];
|
||||
run_migrations(text, migrations)
|
||||
}
|
||||
@@ -180,10 +176,6 @@ define_query!(
|
||||
KEYMAP_QUERY_2025_03_06,
|
||||
migrations::m_2025_03_06::KEYMAP_PATTERNS
|
||||
);
|
||||
define_query!(
|
||||
KEYMAP_QUERY_2025_04_15,
|
||||
migrations::m_2025_04_15::KEYMAP_PATTERNS
|
||||
);
|
||||
|
||||
// settings
|
||||
define_query!(
|
||||
|
||||
@@ -61,17 +61,14 @@ impl Anchor {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
|
||||
let self_excerpt_id = snapshot.latest_excerpt_id(self.excerpt_id);
|
||||
let other_excerpt_id = snapshot.latest_excerpt_id(other.excerpt_id);
|
||||
|
||||
let excerpt_id_cmp = self_excerpt_id.cmp(&other_excerpt_id, snapshot);
|
||||
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
|
||||
if excerpt_id_cmp.is_ne() {
|
||||
return excerpt_id_cmp;
|
||||
}
|
||||
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
|
||||
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
|
||||
if text_cmp.is_ne() {
|
||||
return text_cmp;
|
||||
|
||||
@@ -5170,7 +5170,6 @@ impl MultiBufferSnapshot {
|
||||
excerpt_id: ExcerptId,
|
||||
text_anchor: text::Anchor,
|
||||
) -> Option<Anchor> {
|
||||
let excerpt_id = self.latest_excerpt_id(excerpt_id);
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
@@ -6042,7 +6041,7 @@ impl MultiBufferSnapshot {
|
||||
return &entry.locator;
|
||||
}
|
||||
}
|
||||
panic!("invalid excerpt id {id:?}")
|
||||
panic!("invalid excerpt id {:?}", id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -746,20 +746,19 @@ fn test_expand_excerpts(cx: &mut App) {
|
||||
drop(snapshot);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
let line_zero = multibuffer.snapshot(cx).anchor_before(Point::new(0, 0));
|
||||
multibuffer.expand_excerpts(
|
||||
multibuffer.excerpt_ids(),
|
||||
1,
|
||||
ExpandExcerptDirection::UpAndDown,
|
||||
cx,
|
||||
);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let line_two = snapshot.anchor_before(Point::new(2, 0));
|
||||
assert_eq!(line_two.cmp(&line_zero, &snapshot), cmp::Ordering::Greater);
|
||||
)
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
// Expanding context lines causes the line containing 'fff' to appear in two different excerpts.
|
||||
// We don't attempt to merge them, because removing the excerpt could create inconsistency with other layers
|
||||
// that are tracking excerpt ids.
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
concat!(
|
||||
|
||||
@@ -70,7 +70,7 @@ actions!(
|
||||
ExpandAllEntries,
|
||||
ExpandSelectedEntry,
|
||||
FoldDirectory,
|
||||
OpenSelectedEntry,
|
||||
Open,
|
||||
RevealInFileManager,
|
||||
SelectParent,
|
||||
ToggleActiveEditorPin,
|
||||
@@ -922,12 +922,7 @@ impl OutlinePanel {
|
||||
self.update_cached_entries(None, window, cx);
|
||||
}
|
||||
|
||||
fn open_selected_entry(
|
||||
&mut self,
|
||||
_: &OpenSelectedEntry,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.filter_editor.focus_handle(cx).is_focused(window) {
|
||||
cx.propagate()
|
||||
} else if let Some(selected_entry) = self.selected_entry().cloned() {
|
||||
@@ -4911,7 +4906,7 @@ impl Render for OutlinePanel {
|
||||
}
|
||||
}))
|
||||
.key_context(self.dispatch_context(window, cx))
|
||||
.on_action(cx.listener(Self::open_selected_entry))
|
||||
.on_action(cx.listener(Self::open))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
@@ -5682,7 +5677,7 @@ mod tests {
|
||||
});
|
||||
|
||||
outline_panel.update_in(cx, |outline_panel, window, cx| {
|
||||
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
|
||||
outline_panel.open(&Open, window, cx);
|
||||
});
|
||||
outline_panel.update(cx, |_outline_panel, cx| {
|
||||
assert_eq!(
|
||||
@@ -5857,7 +5852,7 @@ mod tests {
|
||||
|
||||
outline_panel.update_in(cx, |outline_panel, window, cx| {
|
||||
outline_panel.select_previous(&SelectPrevious, window, cx);
|
||||
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
|
||||
outline_panel.open(&Open, window, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
@@ -5881,7 +5876,7 @@ mod tests {
|
||||
|
||||
outline_panel.update_in(cx, |outline_panel, window, cx| {
|
||||
outline_panel.select_next(&SelectNext, window, cx);
|
||||
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
|
||||
outline_panel.open(&Open, window, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
@@ -5902,7 +5897,7 @@ mod tests {
|
||||
});
|
||||
|
||||
outline_panel.update_in(cx, |outline_panel, window, cx| {
|
||||
outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
|
||||
outline_panel.open(&Open, window, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
|
||||
@@ -18,7 +18,7 @@ use text::{Point, PointUtf16};
|
||||
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
|
||||
|
||||
mod breakpoints_in_file {
|
||||
use language::{BufferEvent, DiskState};
|
||||
use language::BufferEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -32,9 +32,8 @@ mod breakpoints_in_file {
|
||||
|
||||
impl BreakpointsInFile {
|
||||
pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
|
||||
let subscription = Arc::from(cx.subscribe(
|
||||
&buffer,
|
||||
|breakpoint_store, buffer, event, cx| match event {
|
||||
let subscription =
|
||||
Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
|
||||
BufferEvent::Saved => {
|
||||
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
|
||||
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
|
||||
@@ -43,44 +42,8 @@ mod breakpoints_in_file {
|
||||
));
|
||||
}
|
||||
}
|
||||
BufferEvent::FileHandleChanged => {
|
||||
let entity_id = buffer.entity_id();
|
||||
|
||||
if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) {
|
||||
breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| {
|
||||
breakpoints_in_file.buffer.entity_id() != entity_id
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
|
||||
if breakpoint_store.breakpoints.contains_key(&abs_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(old_path) = breakpoint_store
|
||||
.breakpoints
|
||||
.iter()
|
||||
.find(|(_, in_file)| in_file.buffer.entity_id() == entity_id)
|
||||
.map(|values| values.0)
|
||||
.cloned()
|
||||
{
|
||||
let Some(breakpoints_in_file) =
|
||||
breakpoint_store.breakpoints.remove(&old_path) else {
|
||||
log::error!("Couldn't get breakpoints in file from old path during buffer rename handling");
|
||||
return;
|
||||
};
|
||||
|
||||
breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
}));
|
||||
|
||||
BreakpointsInFile {
|
||||
buffer,
|
||||
|
||||
@@ -21,10 +21,7 @@ use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
future::{Shared, join_all},
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
|
||||
Task,
|
||||
};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use http_client::HttpClient;
|
||||
use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore};
|
||||
use lsp::LanguageServerName;
|
||||
@@ -93,17 +90,6 @@ impl LocalDapStore {
|
||||
fn next_session_id(&self) -> SessionId {
|
||||
SessionId(self.next_session_id.fetch_add(1, SeqCst))
|
||||
}
|
||||
pub(crate) fn locate_binary(
|
||||
&self,
|
||||
mut definition: DebugTaskDefinition,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Task<DebugTaskDefinition> {
|
||||
let locator_store = self.locator_store.clone();
|
||||
executor.spawn(async move {
|
||||
let _ = locator_store.resolve_debug_config(&mut definition).await;
|
||||
definition
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteDapStore {
|
||||
@@ -349,7 +335,7 @@ impl DapStore {
|
||||
pub fn new_session(
|
||||
&mut self,
|
||||
binary: DebugAdapterBinary,
|
||||
config: DebugTaskDefinition,
|
||||
mut config: DebugTaskDefinition,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (SessionId, Task<Result<Entity<Session>>>) {
|
||||
@@ -366,10 +352,22 @@ impl DapStore {
|
||||
}
|
||||
|
||||
let (initialized_tx, initialized_rx) = oneshot::channel();
|
||||
let locator_store = local_store.locator_store.clone();
|
||||
|
||||
let start_debugging_tx = local_store.start_debugging_tx.clone();
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
if config.locator.is_some() {
|
||||
config = cx
|
||||
.background_spawn(async move {
|
||||
locator_store
|
||||
.resolve_debug_config(&mut config)
|
||||
.await
|
||||
.map(|_| config)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
let start_client_task = this.update(cx, |this, cx| {
|
||||
Session::local(
|
||||
this.breakpoint_store.clone(),
|
||||
@@ -792,48 +790,10 @@ fn create_new_session(
|
||||
this.update(cx, |_, cx| {
|
||||
cx.subscribe(
|
||||
&session,
|
||||
move |this: &mut DapStore, session, event: &SessionStateEvent, cx| match event {
|
||||
move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
|
||||
SessionStateEvent::Shutdown => {
|
||||
this.shutdown_session(session_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
SessionStateEvent::Restart => {
|
||||
let Some((config, binary)) = session.read_with(cx, |session, _| {
|
||||
session
|
||||
.configuration()
|
||||
.map(|config| (config, session.binary().clone()))
|
||||
}) else {
|
||||
log::error!("Failed to get debug config from session");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut curr_session = session;
|
||||
while let Some(parent_id) = curr_session.read(cx).parent_id() {
|
||||
if let Some(parent_session) = this.sessions.get(&parent_id).cloned() {
|
||||
curr_session = parent_session;
|
||||
} else {
|
||||
log::error!("Failed to get parent session from parent session id");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = curr_session.read(cx).session_id();
|
||||
|
||||
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.sessions.remove(&session_id);
|
||||
this.new_session(binary, config, None, cx)
|
||||
})?
|
||||
.1
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use super::DapLocator;
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, json};
|
||||
use smol::{
|
||||
io::AsyncReadExt,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use task::DebugTaskDefinition;
|
||||
use util::maybe;
|
||||
|
||||
pub(super) struct CargoLocator;
|
||||
|
||||
@@ -108,13 +109,43 @@ impl DapLocator for CargoLocator {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let Some(executable) = executable.or_else(|| executables.first().cloned()) else {
|
||||
return Err(anyhow!("Couldn't get executable in cargo locator"));
|
||||
};
|
||||
|
||||
launch_config.program = executable;
|
||||
|
||||
if debug_config.adapter == "LLDB" && debug_config.initialize_args.is_none() {
|
||||
// Find Rust pretty-printers in current toolchain's sysroot
|
||||
let cwd = launch_config.cwd.clone();
|
||||
debug_config.initialize_args = maybe!(async move {
|
||||
let cwd = cwd?;
|
||||
|
||||
let output = Command::new("rustc")
|
||||
.arg("--print")
|
||||
.arg("sysroot")
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sysroot_path = String::from_utf8(output.stdout).ok()?;
|
||||
let sysroot_path = sysroot_path.trim_end();
|
||||
let first_command = format!(
|
||||
r#"command script import "{sysroot_path}/lib/rustlib/etc/lldb_lookup.py"#
|
||||
);
|
||||
let second_command =
|
||||
format!(r#"command source -s 0 '{sysroot_path}/lib/rustlib/etc/lldb_commands"#);
|
||||
|
||||
Some(json!({"initCommands": [first_command, second_command]}))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
launch_config.args.clear();
|
||||
if let Some(test_name) = test_name {
|
||||
launch_config.args.push(test_name);
|
||||
|
||||
@@ -397,7 +397,6 @@ impl LocalMode {
|
||||
self.definition.initialize_args.clone().unwrap_or(json!({})),
|
||||
&mut raw.configuration,
|
||||
);
|
||||
|
||||
// Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
|
||||
let launch = match raw.request {
|
||||
dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(
|
||||
@@ -685,9 +684,8 @@ pub enum SessionEvent {
|
||||
Threads,
|
||||
}
|
||||
|
||||
pub(super) enum SessionStateEvent {
|
||||
pub(crate) enum SessionStateEvent {
|
||||
Shutdown,
|
||||
Restart,
|
||||
}
|
||||
|
||||
impl EventEmitter<SessionEvent> for Session {}
|
||||
@@ -1364,18 +1362,6 @@ impl Session {
|
||||
&self.loaded_sources
|
||||
}
|
||||
|
||||
fn fallback_to_manual_restart(
|
||||
&mut self,
|
||||
res: Result<()>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
if res.log_err().is_none() {
|
||||
cx.emit(SessionStateEvent::Restart);
|
||||
return None;
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn empty_response(&mut self, res: Result<()>, _cx: &mut Context<Self>) -> Option<()> {
|
||||
res.log_err()?;
|
||||
Some(())
|
||||
@@ -1435,17 +1421,26 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn restart(&mut self, args: Option<Value>, cx: &mut Context<Self>) {
|
||||
if self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated() {
|
||||
if self.capabilities.supports_restart_request.unwrap_or(false) {
|
||||
self.request(
|
||||
RestartCommand {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
},
|
||||
Self::fallback_to_manual_restart,
|
||||
Self::empty_response,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
} else {
|
||||
cx.emit(SessionStateEvent::Restart);
|
||||
self.request(
|
||||
DisconnectCommand {
|
||||
restart: Some(false),
|
||||
terminate_debuggee: Some(true),
|
||||
suspend_debuggee: Some(false),
|
||||
},
|
||||
Self::empty_response,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1480,14 +1475,8 @@ impl Session {
|
||||
|
||||
cx.emit(SessionStateEvent::Shutdown);
|
||||
|
||||
let debug_client = self.adapter_client();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let _ = task.await;
|
||||
|
||||
if let Some(client) = debug_client {
|
||||
client.shutdown().await.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1482,18 +1482,6 @@ impl Project {
|
||||
.update(cx, |dap_store, cx| dap_store.delegate(&worktree, cx))
|
||||
})?;
|
||||
|
||||
let task = this.update(cx, |project, cx| {
|
||||
project.dap_store.read(cx).as_local().and_then(|local| {
|
||||
config.locator.is_some().then(|| {
|
||||
local.locate_binary(config.clone(), cx.background_executor().clone())
|
||||
})
|
||||
})
|
||||
})?;
|
||||
let config = if let Some(task) = task {
|
||||
task.await
|
||||
} else {
|
||||
config
|
||||
};
|
||||
let binary = adapter
|
||||
.get_binary(&delegate, &config, user_installed_path, cx)
|
||||
.await?;
|
||||
@@ -3094,9 +3082,6 @@ impl Project {
|
||||
.map(|lister| lister.term())
|
||||
}
|
||||
|
||||
pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
|
||||
self.toolchain_store.clone()
|
||||
}
|
||||
pub fn activate_toolchain(
|
||||
&self,
|
||||
path: ProjectPath,
|
||||
|
||||
@@ -8273,34 +8273,17 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
|
||||
json!({
|
||||
".git": {
|
||||
"worktrees": {
|
||||
"some-worktree": {
|
||||
"commondir": "../..\n"
|
||||
}
|
||||
"some-worktree": {}
|
||||
},
|
||||
"modules": {
|
||||
"subdir": {
|
||||
"some-submodule": {
|
||||
// For is_git_dir
|
||||
"HEAD": "",
|
||||
"config": "",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"a.txt": "A",
|
||||
},
|
||||
"some-worktree": {
|
||||
".git": "gitdir: ../.git/worktrees/some-worktree\n",
|
||||
".git": "gitdir: ../.git/worktrees/some-worktree",
|
||||
"src": {
|
||||
"b.txt": "B",
|
||||
}
|
||||
},
|
||||
"subdir": {
|
||||
"some-submodule": {
|
||||
".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
|
||||
"c.txt": "C",
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -8332,11 +8315,9 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
|
||||
[
|
||||
Path::new(path!("/project")).into(),
|
||||
Path::new(path!("/project/some-worktree")).into(),
|
||||
Path::new(path!("/project/subdir/some-submodule")).into(),
|
||||
]
|
||||
);
|
||||
|
||||
// Generate a git-related event for the worktree and check that it's refreshed.
|
||||
fs.with_git_state(
|
||||
path!("/project/some-worktree/.git").as_ref(),
|
||||
true,
|
||||
@@ -8378,45 +8359,6 @@ async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
|
||||
// The same for the submodule.
|
||||
fs.with_git_state(
|
||||
path!("/project/subdir/some-submodule/.git").as_ref(),
|
||||
true,
|
||||
|state| {
|
||||
state.head_contents.insert("c.txt".into(), "c".to_owned());
|
||||
state.index_contents.insert("c.txt".into(), "c".to_owned());
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let (submodule_repo, barrier) = project.update(cx, |project, cx| {
|
||||
let (repo, _) = project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
.unwrap();
|
||||
pretty_assertions::assert_eq!(
|
||||
repo.read(cx).work_directory_abs_path,
|
||||
Path::new(path!("/project/subdir/some-submodule")).into(),
|
||||
);
|
||||
let barrier = repo.update(cx, |repo, _| repo.barrier());
|
||||
(repo.clone(), barrier)
|
||||
});
|
||||
barrier.await.unwrap();
|
||||
submodule_repo.update(cx, |repo, _| {
|
||||
pretty_assertions::assert_eq!(
|
||||
repo.status_for_path(&"c.txt".into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -55,7 +55,6 @@ impl ToolchainStore {
|
||||
});
|
||||
Self(ToolchainStoreInner::Local(entity, subscription))
|
||||
}
|
||||
|
||||
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
|
||||
Self(ToolchainStoreInner::Remote(
|
||||
cx.new(|_| RemoteToolchainStore { client, project_id }),
|
||||
@@ -286,7 +285,7 @@ struct LocalStore(WeakEntity<LocalToolchainStore>);
|
||||
struct RemoteStore(WeakEntity<RemoteToolchainStore>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ToolchainStoreEvent {
|
||||
pub(crate) enum ToolchainStoreEvent {
|
||||
ToolchainActivated,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ message GetPrivateUserInfoResponse {
|
||||
enum Plan {
|
||||
Free = 0;
|
||||
ZedPro = 1;
|
||||
ZedProTrial = 2;
|
||||
}
|
||||
|
||||
message UpdateUserPlan {
|
||||
|
||||
@@ -1216,7 +1216,7 @@ impl TextDimension for TextSummary {
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize {
|
||||
fn zero(_cx: &()) -> Self {
|
||||
0
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
|
||||
|
||||
35
crates/rpc/src/llm.rs
Normal file
35
crates/rpc/src/llm.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
|
||||
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||
|
||||
pub const MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME: &str = "x-zed-llm-max-monthly-spend-reached";
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LanguageModelProvider {
|
||||
Anthropic,
|
||||
OpenAi,
|
||||
Google,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageModel {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListModelsResponse {
|
||||
pub models: Vec<LanguageModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PerformCompletionParams {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: Box<serde_json::value::RawValue>,
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
pub mod auth;
|
||||
mod conn;
|
||||
mod extension;
|
||||
mod llm;
|
||||
mod message_stream;
|
||||
mod notification;
|
||||
mod peer;
|
||||
|
||||
pub use conn::Connection;
|
||||
pub use extension::*;
|
||||
pub use llm::*;
|
||||
pub use notification::*;
|
||||
pub use peer::*;
|
||||
pub use proto;
|
||||
|
||||
@@ -372,7 +372,13 @@ where
|
||||
"Must call `seek`, `next` or `prev` before calling this method"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, D> Cursor<'a, T, D>
|
||||
where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn seek<Target>(
|
||||
&mut self,
|
||||
|
||||
@@ -98,6 +98,62 @@ impl DebugRequestDisposition {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Represents the configuration for the debug adapter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct DebugAdapterConfig {
|
||||
/// Name of the debug task
|
||||
pub label: String,
|
||||
/// The type of adapter you want to use
|
||||
pub adapter: String,
|
||||
/// The type of request that should be called on the debug adapter
|
||||
pub request: DebugRequestDisposition,
|
||||
/// Additional initialization arguments to be sent on DAP initialization
|
||||
pub initialize_args: Option<serde_json::Value>,
|
||||
/// Optional TCP connection information
|
||||
///
|
||||
/// If provided, this will be used to connect to the debug adapter instead of
|
||||
/// spawning a new process. This is useful for connecting to a debug adapter
|
||||
/// that is already running or is started by another process.
|
||||
pub tcp_connection: Option<TCPHost>,
|
||||
/// What Locator to use to configure the debug task
|
||||
pub locator: Option<String>,
|
||||
/// Whether to tell the debug adapter to stop on entry
|
||||
pub stop_on_entry: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<DebugTaskDefinition> for DebugAdapterConfig {
|
||||
fn from(def: DebugTaskDefinition) -> Self {
|
||||
Self {
|
||||
label: def.label,
|
||||
adapter: def.adapter,
|
||||
request: DebugRequestDisposition::UserConfigured(def.request),
|
||||
initialize_args: def.initialize_args,
|
||||
tcp_connection: def.tcp_connection,
|
||||
locator: def.locator,
|
||||
stop_on_entry: def.stop_on_entry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
|
||||
type Error = ();
|
||||
fn try_from(def: DebugAdapterConfig) -> Result<Self, Self::Error> {
|
||||
let request = match def.request {
|
||||
DebugRequestDisposition::UserConfigured(debug_request_type) => debug_request_type,
|
||||
DebugRequestDisposition::ReverseRequest(_) => return Err(()),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
label: def.label,
|
||||
adapter: def.adapter,
|
||||
request,
|
||||
initialize_args: def.initialize_args,
|
||||
tcp_connection: def.tcp_connection,
|
||||
locator: def.locator,
|
||||
stop_on_entry: def.stop_on_entry,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TaskTemplate> for DebugTaskDefinition {
|
||||
type Error = ();
|
||||
|
||||
@@ -16,8 +16,8 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use debug_format::{
|
||||
AttachConfig, DebugConnectionType, DebugRequestDisposition, DebugRequestType,
|
||||
DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
|
||||
AttachConfig, DebugAdapterConfig, DebugConnectionType, DebugRequestDisposition,
|
||||
DebugRequestType, DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
|
||||
};
|
||||
pub use task_template::{
|
||||
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
|
||||
|
||||
@@ -36,7 +36,7 @@ use ui::{
|
||||
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
|
||||
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
|
||||
|
||||
pub use onboarding_banner::restore_banner;
|
||||
@@ -210,6 +210,7 @@ impl Render for TitleBar {
|
||||
.pr_1()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.children(self.render_call_controls(window, cx))
|
||||
.child(self.render_bottom_dock_layout_menu(cx))
|
||||
.map(|el| {
|
||||
let status = self.client.status();
|
||||
let status = &*status.borrow();
|
||||
@@ -301,7 +302,7 @@ impl TitleBar {
|
||||
cx.notify()
|
||||
}),
|
||||
);
|
||||
subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
|
||||
subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
|
||||
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
|
||||
@@ -622,6 +623,101 @@ impl TitleBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let workspace = self.workspace.upgrade().unwrap();
|
||||
let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
|
||||
|
||||
PopoverMenu::new("layout-menu")
|
||||
.trigger(
|
||||
IconButton::new("toggle_layout", IconName::Layout)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Toggle Layout Menu")),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, {
|
||||
let workspace = workspace.clone();
|
||||
move |menu, _, _| {
|
||||
menu.label("Bottom Dock")
|
||||
.separator()
|
||||
.toggleable_entry(
|
||||
"Contained",
|
||||
current_layout == BottomDockLayout::Contained,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Contained,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Full",
|
||||
current_layout == BottomDockLayout::Full,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Full,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Left Aligned",
|
||||
current_layout == BottomDockLayout::LeftAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::LeftAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Right Aligned",
|
||||
current_layout == BottomDockLayout::RightAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::RightAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
|
||||
let client = self.client.clone();
|
||||
Button::new("sign_in", "Sign in")
|
||||
@@ -655,7 +751,6 @@ impl TitleBar {
|
||||
None => "",
|
||||
Some(proto::Plan::Free) => "Free",
|
||||
Some(proto::Plan::ZedPro) => "Pro",
|
||||
Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
|
||||
}
|
||||
),
|
||||
zed_actions::OpenAccountSettings.boxed_clone(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
WeakEntity, Window, div,
|
||||
};
|
||||
use language::{Buffer, BufferEvent, LanguageName, Toolchain};
|
||||
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
||||
@@ -22,28 +22,6 @@ pub struct ActiveToolchain {
|
||||
|
||||
impl ActiveToolchain {
|
||||
pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
if let Some(store) = workspace.project().read(cx).toolchain_store() {
|
||||
cx.subscribe_in(
|
||||
&store,
|
||||
window,
|
||||
|this, _, _: &ToolchainStoreEvent, window, cx| {
|
||||
let editor = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(editor) = editor {
|
||||
this.active_toolchain.take();
|
||||
this.update_lister(editor, window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
Self {
|
||||
active_toolchain: None,
|
||||
active_buffer: None,
|
||||
@@ -79,19 +57,12 @@ impl ActiveToolchain {
|
||||
this.term = term;
|
||||
cx.notify();
|
||||
});
|
||||
let (worktree_id, path) = active_file
|
||||
.update(cx, |this, cx| {
|
||||
this.file().and_then(|file| {
|
||||
Some((
|
||||
file.worktree_id(cx),
|
||||
Arc::<Path>::from(file.path().parent()?),
|
||||
))
|
||||
})
|
||||
})
|
||||
let worktree_id = active_file
|
||||
.update(cx, |this, cx| Some(this.file()?.worktree_id(cx)))
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let toolchain =
|
||||
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
|
||||
Self::active_toolchain(workspace, worktree_id, language_name, cx).await?;
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.active_toolchain = Some(toolchain);
|
||||
|
||||
@@ -130,7 +101,6 @@ impl ActiveToolchain {
|
||||
fn active_toolchain(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
worktree_id: WorktreeId,
|
||||
relative_path: Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Task<Option<Toolchain>> {
|
||||
@@ -144,7 +114,7 @@ impl ActiveToolchain {
|
||||
this.project().read(cx).active_toolchain(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path.clone(),
|
||||
path: Arc::from("".as_ref()),
|
||||
},
|
||||
language_name.clone(),
|
||||
cx,
|
||||
@@ -163,7 +133,7 @@ impl ActiveToolchain {
|
||||
project.read(cx).available_toolchains(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path.clone(),
|
||||
path: Arc::from("".as_ref()),
|
||||
},
|
||||
language_name,
|
||||
cx,
|
||||
@@ -174,12 +144,7 @@ impl ActiveToolchain {
|
||||
if let Some(toolchain) = toolchains.toolchains.first() {
|
||||
// Since we don't have a selected toolchain, pick one for user here.
|
||||
workspace::WORKSPACE_DB
|
||||
.set_toolchain(
|
||||
workspace_id,
|
||||
worktree_id,
|
||||
relative_path.to_string_lossy().into_owned(),
|
||||
toolchain.clone(),
|
||||
)
|
||||
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
|
||||
.await
|
||||
.ok()?;
|
||||
project
|
||||
@@ -187,7 +152,7 @@ impl ActiveToolchain {
|
||||
this.activate_toolchain(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path,
|
||||
path: Arc::from("".as_ref()),
|
||||
},
|
||||
toolchain.clone(),
|
||||
cx,
|
||||
|
||||
@@ -50,7 +50,6 @@ impl ToolchainSelector {
|
||||
|
||||
let language_name = buffer.read(cx).language()?.name();
|
||||
let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
|
||||
let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
|
||||
let worktree_root_path = project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)?
|
||||
@@ -59,9 +58,8 @@ impl ToolchainSelector {
|
||||
let workspace_id = workspace.database_id()?;
|
||||
let weak = workspace.weak_handle();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let as_str = relative_path.to_string_lossy().into_owned();
|
||||
let active_toolchain = workspace::WORKSPACE_DB
|
||||
.toolchain(workspace_id, worktree_id, as_str, language_name.clone())
|
||||
.toolchain(workspace_id, worktree_id, language_name.clone())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
@@ -74,7 +72,6 @@ impl ToolchainSelector {
|
||||
active_toolchain,
|
||||
worktree_id,
|
||||
worktree_root_path,
|
||||
relative_path,
|
||||
language_name,
|
||||
window,
|
||||
cx,
|
||||
@@ -94,7 +91,6 @@ impl ToolchainSelector {
|
||||
active_toolchain: Option<Toolchain>,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_root: Arc<Path>,
|
||||
relative_path: Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -108,7 +104,6 @@ impl ToolchainSelector {
|
||||
worktree_id,
|
||||
worktree_root,
|
||||
project,
|
||||
relative_path,
|
||||
language_name,
|
||||
window,
|
||||
cx,
|
||||
@@ -142,7 +137,6 @@ pub struct ToolchainSelectorDelegate {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_abs_path_root: Arc<Path>,
|
||||
relative_path: Arc<Path>,
|
||||
placeholder_text: Arc<str>,
|
||||
_fetch_candidates_task: Task<Option<()>>,
|
||||
}
|
||||
@@ -155,7 +149,6 @@ impl ToolchainSelectorDelegate {
|
||||
worktree_id: WorktreeId,
|
||||
worktree_abs_path_root: Arc<Path>,
|
||||
project: Entity<Project>,
|
||||
relative_path: Arc<Path>,
|
||||
language_name: LanguageName,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
@@ -169,26 +162,17 @@ impl ToolchainSelectorDelegate {
|
||||
})
|
||||
.ok()?
|
||||
.await?;
|
||||
let relative_path = this
|
||||
.update(cx, |this, _| this.delegate.relative_path.clone())
|
||||
.ok()?;
|
||||
let placeholder_text = format!(
|
||||
"Select a {} for `{}`…",
|
||||
term.to_lowercase(),
|
||||
relative_path.to_string_lossy()
|
||||
)
|
||||
.into();
|
||||
let placeholder_text = format!("Select a {}…", term.to_lowercase()).into();
|
||||
let _ = this.update_in(cx, move |this, window, cx| {
|
||||
this.delegate.placeholder_text = placeholder_text;
|
||||
this.refresh_placeholder(window, cx);
|
||||
});
|
||||
|
||||
let available_toolchains = project
|
||||
.update(cx, |this, cx| {
|
||||
this.available_toolchains(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path.clone(),
|
||||
path: Arc::from("".as_ref()),
|
||||
},
|
||||
language_name,
|
||||
cx,
|
||||
@@ -227,7 +211,6 @@ impl ToolchainSelectorDelegate {
|
||||
worktree_id,
|
||||
worktree_abs_path_root,
|
||||
placeholder_text,
|
||||
relative_path,
|
||||
_fetch_candidates_task,
|
||||
}
|
||||
}
|
||||
@@ -263,18 +246,19 @@ impl PickerDelegate for ToolchainSelectorDelegate {
|
||||
{
|
||||
let workspace = self.workspace.clone();
|
||||
let worktree_id = self.worktree_id;
|
||||
let path = self.relative_path.clone();
|
||||
let relative_path = self.relative_path.to_string_lossy().into_owned();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
workspace::WORKSPACE_DB
|
||||
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
|
||||
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
|
||||
.await
|
||||
.log_err();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.project().update(cx, |this, cx| {
|
||||
this.activate_toolchain(
|
||||
ProjectPath { worktree_id, path },
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from("".as_ref()),
|
||||
},
|
||||
toolchain,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -73,11 +73,11 @@ impl Tab {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn content_height(cx: &App) -> Pixels {
|
||||
pub fn content_height(cx: &mut App) -> Pixels {
|
||||
DynamicSpacing::Base32.px(cx) - px(1.)
|
||||
}
|
||||
|
||||
pub fn container_height(cx: &App) -> Pixels {
|
||||
pub fn container_height(cx: &mut App) -> Pixels {
|
||||
DynamicSpacing::Base32.px(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,11 +160,7 @@ impl Render for Tooltip {
|
||||
}),
|
||||
)
|
||||
.when_some(self.meta.clone(), |this, meta| {
|
||||
this.child(
|
||||
div()
|
||||
.max_w_72()
|
||||
.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user