Compare commits
2 Commits
revert-bin
...
agent-bann
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7cb23fe78 | ||
|
|
c294b4d0b8 |
2
.github/workflows/run_agent_eval_daily.yml
vendored
2
.github/workflows/run_agent_eval_daily.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
166
Cargo.lock
generated
166
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"
|
||||
@@ -448,6 +448,7 @@ dependencies = [
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -566,7 +567,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -703,7 +704,6 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
@@ -721,11 +721,9 @@ dependencies = [
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"web_search",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1883,7 +1881,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
@@ -3030,7 +3028,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
@@ -3050,7 +3048,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3200,6 +3197,7 @@ dependencies = [
|
||||
"serde",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_parking_lot",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -3363,7 +3361,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -4480,7 +4478,7 @@ dependencies = [
|
||||
"optfield",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
@@ -4919,6 +4917,36 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evals"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"dap",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"languages",
|
||||
"node_runtime",
|
||||
"open_ai",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"semantic_index",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@@ -5095,7 +5123,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5946,7 +5974,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -6039,7 +6067,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -6145,7 +6173,7 @@ dependencies = [
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"thiserror 2.0.12",
|
||||
@@ -6793,7 +6821,7 @@ name = "icons"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -7061,7 +7089,7 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -7647,7 +7675,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
@@ -7707,7 +7735,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"tiktoken-rs",
|
||||
@@ -7715,7 +7743,6 @@ dependencies = [
|
||||
"ui",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8680,7 +8707,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -9527,7 +9554,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -11721,7 +11748,6 @@ name = "remote_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-watch",
|
||||
"backtrace",
|
||||
"cargo_toml",
|
||||
@@ -12107,7 +12133,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"tracing",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -12635,7 +12661,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -13300,7 +13326,6 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"schemars",
|
||||
@@ -13681,7 +13706,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"story",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
@@ -13763,16 +13788,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]]
|
||||
@@ -13788,19 +13804,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"
|
||||
@@ -14416,7 +14419,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -14450,7 +14453,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"simplelog",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"vscode_theme",
|
||||
"workspace-hack",
|
||||
@@ -15451,7 +15454,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"theme",
|
||||
"ui_macros",
|
||||
"util",
|
||||
@@ -15485,6 +15488,19 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ui_parking_lot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"component",
|
||||
"gpui",
|
||||
"linkme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ui_prompt"
|
||||
version = "0.1.0"
|
||||
@@ -16584,36 +16600,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"
|
||||
@@ -17652,7 +17638,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sqlez",
|
||||
"strum 0.27.1",
|
||||
"strum",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
@@ -17797,7 +17783,7 @@ dependencies = [
|
||||
"sqlx-macros-core",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"strum 0.26.3",
|
||||
"strum",
|
||||
"subtle",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.100",
|
||||
@@ -18175,7 +18161,6 @@ dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"ashpd",
|
||||
"askpass",
|
||||
"assets",
|
||||
"assistant",
|
||||
"assistant_context_editor",
|
||||
@@ -18293,8 +18278,6 @@ dependencies = [
|
||||
"uuid",
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"welcome",
|
||||
"windows 0.61.1",
|
||||
"winresource",
|
||||
@@ -18359,13 +18342,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",
|
||||
]
|
||||
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -47,6 +47,7 @@ members = [
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/evals",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -157,6 +158,7 @@ members = [
|
||||
"crates/title_bar",
|
||||
"crates/toolchain_selector",
|
||||
"crates/ui",
|
||||
"crates/ui_parking_lot",
|
||||
"crates/ui_input",
|
||||
"crates/ui_macros",
|
||||
"crates/ui_prompt",
|
||||
@@ -164,8 +166,6 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -366,13 +366,12 @@ toolchain_selector = { path = "crates/toolchain_selector" }
|
||||
ui = { path = "crates/ui" }
|
||||
ui_input = { path = "crates/ui_input" }
|
||||
ui_macros = { path = "crates/ui_macros" }
|
||||
ui_parking_lot = { path = "crates/ui_parking_lot" }
|
||||
ui_prompt = { path = "crates/ui_prompt" }
|
||||
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" }
|
||||
@@ -539,7 +538,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"
|
||||
@@ -604,7 +603,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"
|
||||
|
||||
@@ -695,6 +694,7 @@ breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
evals = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
feature_flags = { codegen-units = 1 }
|
||||
file_icons = { codegen-units = 1 }
|
||||
|
||||
@@ -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 {
|
||||
@@ -756,10 +750,6 @@ impl ActiveThread {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn context_store(&self) -> &Entity<ContextStore> {
|
||||
&self.context_store
|
||||
}
|
||||
|
||||
pub fn thread(&self) -> &Entity<Thread> {
|
||||
&self.thread
|
||||
}
|
||||
@@ -790,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,
|
||||
@@ -960,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,
|
||||
);
|
||||
@@ -1142,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();
|
||||
@@ -1495,12 +1402,12 @@ impl ActiveThread {
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click(|_, window, cx| {
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
|
||||
});
|
||||
|
||||
@@ -1768,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(
|
||||
@@ -1833,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;
|
||||
@@ -2383,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
|
||||
@@ -2450,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| {
|
||||
@@ -2479,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()
|
||||
}),
|
||||
)),
|
||||
),
|
||||
@@ -2545,7 +2431,6 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
@@ -2659,7 +2544,7 @@ impl ActiveThread {
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.my_2()
|
||||
.my_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -2876,7 +2761,7 @@ impl ActiveThread {
|
||||
)
|
||||
})
|
||||
}
|
||||
}).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -3068,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()
|
||||
@@ -3149,21 +3028,28 @@ pub(crate) fn open_context(
|
||||
.start
|
||||
.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if let Some(project_path) = excerpt_context
|
||||
.context_buffer
|
||||
.buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
{
|
||||
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
|
||||
let target_position = excerpt_context.range.start.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target_position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -3184,29 +3070,3 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_editor_at_position(
|
||||
project_path: project::ProjectPath,
|
||||
target_position: Point,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<()> {
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(target_position, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -6,14 +5,14 @@ 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;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use client::zed_urls;
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
|
||||
@@ -26,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::{
|
||||
@@ -38,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;
|
||||
@@ -113,9 +111,7 @@ enum ActiveView {
|
||||
change_title_editor: Entity<Editor>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
PromptEditor {
|
||||
context_editor: Entity<ContextEditor>,
|
||||
},
|
||||
PromptEditor,
|
||||
History,
|
||||
Configuration,
|
||||
}
|
||||
@@ -184,9 +180,10 @@ 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>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
@@ -266,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));
|
||||
|
||||
@@ -297,12 +287,6 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -311,13 +295,10 @@ 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,
|
||||
configuration_subscription: None,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
@@ -400,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(),
|
||||
@@ -419,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(
|
||||
@@ -438,22 +412,11 @@ 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>) {
|
||||
self.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
@@ -461,7 +424,7 @@ impl AssistantPanel {
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
let context_editor = cx.new(|cx| {
|
||||
self.context_editor = Some(cx.new(|cx| {
|
||||
let mut editor = ContextEditor::for_context(
|
||||
context,
|
||||
self.fs.clone(),
|
||||
@@ -473,16 +436,11 @@ impl AssistantPanel {
|
||||
);
|
||||
editor.insert_default_prompt(window, cx);
|
||||
editor
|
||||
});
|
||||
}));
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: context_editor.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_prompt_library(
|
||||
@@ -549,13 +507,8 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: editor,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
this.context_editor = Some(editor);
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
@@ -584,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(),
|
||||
@@ -602,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(),
|
||||
@@ -622,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,
|
||||
];
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -786,15 +711,8 @@ impl AssistantPanel {
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn has_active_thread(&self) -> bool {
|
||||
matches!(self.active_view, ActiveView::Thread { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
||||
match &self.active_view {
|
||||
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
|
||||
_ => None,
|
||||
}
|
||||
self.context_editor.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn delete_context(
|
||||
@@ -832,10 +750,16 @@ impl AssistantPanel {
|
||||
|
||||
impl Focusable for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.active_view {
|
||||
match self.active_view {
|
||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
|
||||
ActiveView::PromptEditor => {
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx)
|
||||
} else {
|
||||
cx.focus_handle()
|
||||
}
|
||||
}
|
||||
ActiveView::Configuration => {
|
||||
if let Some(configuration) = self.configuration.as_ref() {
|
||||
configuration.focus_handle(cx)
|
||||
@@ -928,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 {
|
||||
@@ -959,8 +883,15 @@ impl AssistantPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor { context_editor } => {
|
||||
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
|
||||
ActiveView::PromptEditor => {
|
||||
let title = self
|
||||
.context_editor
|
||||
.as_ref()
|
||||
.map(|context_editor| {
|
||||
SharedString::from(context_editor.read(cx).title(cx).to_string())
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
|
||||
|
||||
Label::new(title).ml_2().truncate().into_any_element()
|
||||
}
|
||||
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
||||
@@ -981,18 +912,21 @@ 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 {
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
ActiveView::PromptEditor { .. } => true,
|
||||
ActiveView::PromptEditor => self.context_editor.is_some(),
|
||||
_ => 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(
|
||||
@@ -1039,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()
|
||||
@@ -1118,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(),
|
||||
)
|
||||
},
|
||||
))
|
||||
@@ -1137,110 +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 { context_editor } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1559,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)
|
||||
}
|
||||
@@ -1664,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,
|
||||
@@ -1771,7 +1593,7 @@ impl AssistantPanel {
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
|
||||
if matches!(self.active_view, ActiveView::PromptEditor) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
@@ -1799,13 +1621,13 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
|
||||
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
})
|
||||
}
|
||||
@@ -1870,7 +1692,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Entity<ContextEditor>> {
|
||||
let panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
panel.read(cx).active_context_editor()
|
||||
panel.update(cx, |panel, _cx| panel.context_editor.clone())
|
||||
}
|
||||
|
||||
fn open_saved_context(
|
||||
@@ -1901,59 +1723,10 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
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 panel.has_active_thread() {
|
||||
panel.thread.update(cx, |thread, cx| {
|
||||
thread.context_store().update(cx, |store, cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.flat_map(|range| {
|
||||
let (start_buffer, start) =
|
||||
buffer.text_anchor_for_position(range.start, cx)?;
|
||||
let (end_buffer, end) =
|
||||
buffer.text_anchor_for_position(range.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
Some((start_buffer, start..end))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (buffer, range) in selection_ranges {
|
||||
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if let Some(context_editor) = panel.active_context_editor() {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
@@ -24,7 +23,6 @@ pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
Symbol,
|
||||
Excerpt,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
@@ -35,7 +33,6 @@ impl ContextKind {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::Excerpt => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
@@ -49,7 +46,6 @@ pub enum AssistantContext {
|
||||
Symbol(SymbolContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
Excerpt(ExcerptContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -60,7 +56,6 @@ impl AssistantContext {
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
Self::Excerpt(excerpt) => excerpt.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,14 +155,6 @@ pub struct ContextSymbolId {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExcerptContext {
|
||||
pub id: ContextId,
|
||||
pub range: Range<Anchor>,
|
||||
pub line_range: Range<Point>,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
/// Formats a collection of contexts into a string representation
|
||||
pub fn format_context_as_string<'a>(
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
@@ -176,7 +163,6 @@ pub fn format_context_as_string<'a>(
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut symbol_context = Vec::new();
|
||||
let mut excerpt_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
@@ -185,7 +171,6 @@ pub fn format_context_as_string<'a>(
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
}
|
||||
@@ -194,7 +179,6 @@ pub fn format_context_as_string<'a>(
|
||||
if file_context.is_empty()
|
||||
&& directory_context.is_empty()
|
||||
&& symbol_context.is_empty()
|
||||
&& excerpt_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
{
|
||||
@@ -232,15 +216,6 @@ pub fn format_context_as_string<'a>(
|
||||
result.push_str("</symbols>\n");
|
||||
}
|
||||
|
||||
if !excerpt_context.is_empty() {
|
||||
result.push_str("<excerpts>\n");
|
||||
for context in excerpt_context {
|
||||
result.push_str(&context.context_buffer.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</excerpts>\n");
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
result.push_str("<fetched_urls>\n");
|
||||
for context in &fetch_context {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use rope::{Point, Rope};
|
||||
use rope::Rope;
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -110,7 +110,7 @@ impl ContextStore {
|
||||
}
|
||||
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl ContextStore {
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -206,7 +206,7 @@ impl ContextStore {
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
for buffer in buffers.into_iter().flatten() {
|
||||
if let Some((buffer_info, text_task)) =
|
||||
collect_buffer_info_and_text(buffer, cx).log_err()
|
||||
collect_buffer_info_and_text(buffer, None, cx).log_err()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
@@ -290,14 +290,11 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
|
||||
buffer,
|
||||
symbol_enclosing_range.clone(),
|
||||
cx,
|
||||
) {
|
||||
Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let (buffer_info, collect_content_task) =
|
||||
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
|
||||
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = collect_content_task.await;
|
||||
@@ -419,49 +416,6 @@ impl ContextStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_excerpt(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
|
||||
})??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_excerpt(
|
||||
make_context_buffer(buffer_info, text),
|
||||
range,
|
||||
line_range,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_excerpt(
|
||||
&mut self,
|
||||
context_buffer: ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
line_range: Range<Point>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.context.push(AssistantContext::Excerpt(ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn accept_suggested_context(
|
||||
&mut self,
|
||||
suggested: &SuggestedContext,
|
||||
@@ -511,7 +465,6 @@ impl ContextStore {
|
||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||
self.symbols.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Excerpt(_) => {}
|
||||
AssistantContext::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
@@ -639,7 +592,6 @@ impl ContextStore {
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::Excerpt(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_) => None,
|
||||
})
|
||||
@@ -691,78 +643,41 @@ fn make_context_symbol(
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text_for_range(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer
|
||||
.read(cx)
|
||||
.text_for_range(range.clone())
|
||||
.collect::<Rope>();
|
||||
|
||||
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task = cx.background_spawn({
|
||||
let line_range = line_range.clone();
|
||||
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
|
||||
});
|
||||
|
||||
Ok((line_range, buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: &App,
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer.read(cx).as_rope().clone();
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task =
|
||||
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let file = buffer_ref.file().context("file context must have a path")?;
|
||||
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
let version = buffer_ref.version();
|
||||
let content = if let Some(range) = range {
|
||||
buffer_ref.text_for_range(range).collect::<Rope>()
|
||||
} else {
|
||||
buffer_ref.as_rope().clone()
|
||||
};
|
||||
|
||||
Ok(BufferInfo {
|
||||
let buffer_info = BufferInfo {
|
||||
buffer,
|
||||
id: buffer_ref.remote_id(),
|
||||
file: file.clone(),
|
||||
version,
|
||||
})
|
||||
};
|
||||
|
||||
let full_path = file.full_path(cx);
|
||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(
|
||||
path: &Path,
|
||||
content: Rope,
|
||||
line_range: Option<Range<Point>>,
|
||||
) -> SharedString {
|
||||
let line_range_text = line_range.map(|range| {
|
||||
if range.start.row == range.end.row {
|
||||
format!(":{}", range.start.row + 1)
|
||||
} else {
|
||||
format!(":{}-{}", range.start.row + 1, range.end.row + 1)
|
||||
}
|
||||
});
|
||||
|
||||
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
let path_extension = path.extension().and_then(|ext| ext.to_str());
|
||||
let path_string = path.to_string_lossy();
|
||||
let capacity = 3
|
||||
+ path_extension.map_or(0, |extension| extension.len() + 1)
|
||||
+ path_string.len()
|
||||
+ line_range_text.as_ref().map_or(0, |text| text.len())
|
||||
+ 1
|
||||
+ content.len()
|
||||
+ 5;
|
||||
@@ -776,10 +691,6 @@ fn to_fenced_codeblock(
|
||||
}
|
||||
buffer.push_str(&path_string);
|
||||
|
||||
if let Some(line_range_text) = line_range_text {
|
||||
buffer.push_str(&line_range_text);
|
||||
}
|
||||
|
||||
buffer.push('\n');
|
||||
for chunk in content.chunks() {
|
||||
buffer.push_str(&chunk);
|
||||
@@ -858,14 +769,6 @@ pub fn refresh_context_store_text(
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|
||||
{
|
||||
let context_store = context_store.clone();
|
||||
return refresh_excerpt_text(context_store, excerpt_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
if changed_buffers.is_empty() {
|
||||
let context_store = context_store.clone();
|
||||
@@ -977,34 +880,6 @@ fn refresh_symbol_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_excerpt_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
excerpt_context: &ExcerptContext,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let id = excerpt_context.id;
|
||||
let range = excerpt_context.range.clone();
|
||||
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
|
||||
if let Some(task) = task {
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let (line_range, context_buffer) = task.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_excerpt_context = ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_thread_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_context: &ThreadContext,
|
||||
@@ -1033,29 +908,13 @@ fn refresh_context_buffer(
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_excerpt(
|
||||
context_buffer: &ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (line_range, buffer_info, text_task) =
|
||||
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
|
||||
.log_err()?;
|
||||
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_symbol(
|
||||
context_symbol: &ContextSymbol,
|
||||
cx: &App,
|
||||
@@ -1063,9 +922,9 @@ fn refresh_context_symbol(
|
||||
let buffer = context_symbol.buffer.read(cx);
|
||||
let project_path = buffer.project_path(cx)?;
|
||||
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
context_symbol.buffer.clone(),
|
||||
context_symbol.enclosing_range.clone(),
|
||||
Some(context_symbol.enclosing_range.clone()),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
|
||||
@@ -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() {
|
||||
@@ -725,12 +676,6 @@ impl Thread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -883,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(),
|
||||
})
|
||||
@@ -1069,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 =
|
||||
@@ -1094,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;
|
||||
@@ -1206,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>()
|
||||
{
|
||||
@@ -1481,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;
|
||||
@@ -1936,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 {
|
||||
@@ -1978,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(
|
||||
@@ -2029,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,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||
@@ -73,12 +73,7 @@ impl Render for IncompatibleToolsTooltip {
|
||||
.children(
|
||||
self.incompatible_tools
|
||||
.iter()
|
||||
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
|
||||
match tool.source() {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
|
||||
}
|
||||
)),
|
||||
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
|
||||
),
|
||||
)
|
||||
.child(Label::new("What To Do Instead").size(LabelSize::Small))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -299,39 +315,6 @@ impl AddedContext {
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
let full_path = excerpt_context.context_buffer.file.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(
|
||||
" ({}-{})",
|
||||
excerpt_context.line_range.start.row + 1,
|
||||
excerpt_context.line_range.end.row + 1
|
||||
);
|
||||
|
||||
full_path_string.push_str(&line_range_text);
|
||||
name.push_str(&line_range_text);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
AddedContext {
|
||||
id: excerpt_context.id,
|
||||
kind: ContextKind::File, // Use File icon for excerpts
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: Some(full_path_string.into()),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
|
||||
@@ -18,4 +18,5 @@ gpui.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -72,8 +72,6 @@ impl AskPassSession {
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
@@ -112,10 +110,21 @@ impl AskPassSession {
|
||||
drop(temp_dir)
|
||||
});
|
||||
|
||||
anyhow::ensure!(
|
||||
which::which("nc").is_ok(),
|
||||
"Cannot find `nc` command (netcat), which is required to connect over SSH."
|
||||
);
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path.display(),
|
||||
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
|
||||
// on macOS `brew install netcat` provides the GNU netcat implementation
|
||||
// which does not support -U.
|
||||
nc = if cfg!(target_os = "macos") {
|
||||
"/usr/bin/nc"
|
||||
} else {
|
||||
"nc"
|
||||
},
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
@@ -161,51 +170,6 @@ impl AskPassSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
#[cfg(unix)]
|
||||
pub fn main(socket: &str) {
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::exit;
|
||||
|
||||
let mut stream = match UnixStream::connect(socket) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
eprintln!("Error connecting to socket {}: {}", socket, err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
|
||||
eprintln!("Error reading from stdin: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if buffer.last() != Some(&b'\0') {
|
||||
buffer.push(b'\0');
|
||||
}
|
||||
|
||||
if let Err(err) = stream.write_all(&buffer) {
|
||||
eprintln!("Error writing to socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut response = Vec::new();
|
||||
if let Err(err) = stream.read_to_end(&mut response) {
|
||||
eprintln!("Error reading from socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if let Err(err) = io::stdout().write_all(&response) {
|
||||
eprintln!("Error writing to stdout: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
pub fn main(_socket: &str) {}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub struct AskPassSession {
|
||||
path: PathBuf,
|
||||
|
||||
@@ -13,7 +13,7 @@ use assistant_context_editor::{
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
@@ -28,12 +28,9 @@ use language_model::{
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -1416,8 +1413,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
creases: Vec<(String, String)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -1429,12 +1425,6 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
@@ -1443,9 +1433,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
.active_context_editor(cx)
|
||||
.or_else(|| panel.new_context(window, cx))
|
||||
{
|
||||
context.update(cx, |context, cx| {
|
||||
context.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,8 @@ use assistant_slash_commands::{
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
|
||||
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
@@ -155,8 +155,7 @@ pub trait AssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
creases: Vec<(String, String)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
);
|
||||
@@ -1801,42 +1800,23 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((selections, buffer)) = maybe!({
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selections = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.into_iter()
|
||||
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
Some((selections, buffer))
|
||||
}) else {
|
||||
let Some(creases) = selections_creases(workspace, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if selections.is_empty() {
|
||||
if creases.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
|
||||
}
|
||||
|
||||
pub fn quote_ranges(
|
||||
pub fn quote_creases(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
creases: Vec<(String, String)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let creases = selections_creases(ranges, snapshot, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert("\n", window, cx);
|
||||
for (text, crease_title) in creases {
|
||||
|
||||
@@ -3,12 +3,10 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use editor::{Editor, MultiBufferSnapshot};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use rope::Point;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::IconName;
|
||||
@@ -71,22 +69,7 @@ impl SlashCommand for SelectionCommand {
|
||||
let mut events = vec![];
|
||||
|
||||
let Some(creases) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selection_ranges = editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
Some(selections_creases(selection_ranges, snapshot, cx))
|
||||
})
|
||||
})
|
||||
.update(cx, selections_creases)
|
||||
.unwrap_or_else(|e| {
|
||||
events.push(Err(e));
|
||||
None
|
||||
@@ -119,82 +102,94 @@ impl SlashCommand for SelectionCommand {
|
||||
}
|
||||
|
||||
pub fn selections_creases(
|
||||
selection_ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut creases = Vec::new();
|
||||
for range in selection_ranges {
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let start_language = snapshot.language_at(range.start);
|
||||
let end_language = snapshot.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = snapshot
|
||||
.symbols_containing(range.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = snapshot
|
||||
.symbols_containing(range.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
workspace: &mut workspace::Workspace,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Vec<(String, String)>> {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
let mut creases = vec![];
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in selections {
|
||||
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
|
||||
..editor::ToOffset::to_offset(&selection.end, &buffer);
|
||||
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let start_language = buffer.language_at(range.start);
|
||||
let end_language = buffer.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = buffer
|
||||
.file_at(selection.start)
|
||||
.map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = buffer
|
||||
.symbols_containing(selection.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = buffer
|
||||
.symbols_containing(selection.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(selection.start.row..=selection.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) =
|
||||
line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(range.start.row..=range.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = selection.start.row + 1;
|
||||
let end_line = selection.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
}
|
||||
creases
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
}
|
||||
});
|
||||
Some(creases)
|
||||
}
|
||||
|
||||
@@ -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,4 +0,0 @@
|
||||
alter table subscription_usages
|
||||
add column plan text not null;
|
||||
|
||||
create index ix_subscription_usages_on_plan on subscription_usages (plan);
|
||||
@@ -330,10 +330,8 @@ async fn create_billing_subscription(
|
||||
.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?;
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
@@ -1020,20 +1018,8 @@ async fn get_current_usage(
|
||||
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,
|
||||
};
|
||||
let model_requests_limit = Some(500);
|
||||
let edit_prediction_limit = Some(2000);
|
||||
|
||||
Ok(Json(GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::db::UserId;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
@@ -11,7 +10,6 @@ pub struct Model {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,10 +25,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,
|
||||
pub plan: rpc::proto::Plan,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
}
|
||||
@@ -70,6 +70,9 @@ impl LlmTokenClaims {
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
has_predict_edits_feature_flag: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "predict-edits"),
|
||||
has_llm_subscription: has_legacy_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
@@ -78,11 +81,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,
|
||||
},
|
||||
plan,
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::LazyLock;
|
||||
@@ -15,9 +16,20 @@ pub trait Component {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
|
||||
fn init_component(_weak_workspace: Box<dyn Any>) {}
|
||||
|
||||
// In theory we could downcast to a WeakEntity<Workspace> and use that to build
|
||||
// whatever you need to initialize the component, but I haven't tested it yet.
|
||||
//
|
||||
// fn init_component(weak_workspace: Box<dyn Any>) {
|
||||
// let weak_workspace = weak_workspace.downcast::<WeakEntity<Workspace>>().unwrap();
|
||||
// }
|
||||
|
||||
fn name() -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
|
||||
/// Returns a name that the component should be sorted by.
|
||||
///
|
||||
/// Implement this if the component should be sorted in an alternate order than its name.
|
||||
|
||||
@@ -23,6 +23,7 @@ languages.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
ui.workspace = true
|
||||
ui_parking_lot.workspace = true
|
||||
ui_input.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -26,6 +26,9 @@ use ui_input::SingleLineInput;
|
||||
use workspace::{AppState, ItemId, SerializableItem};
|
||||
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
|
||||
|
||||
#[allow(unused)]
|
||||
use ui_parking_lot::*; // Import for component registry, gets culled otherwise
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
workspace::register_serializable_item::<ComponentPreview>(cx);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,7 +33,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},
|
||||
@@ -317,20 +316,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(),
|
||||
@@ -782,6 +769,9 @@ impl DebugPanel {
|
||||
this.restart_session(cx);
|
||||
},
|
||||
))
|
||||
.disabled(
|
||||
!capabilities.supports_restart_request.unwrap_or_default(),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Restart")(window, cx)
|
||||
}),
|
||||
|
||||
@@ -15,7 +15,7 @@ use text::{AnchorRangeExt, Point};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
|
||||
Window, div,
|
||||
Window, div, px,
|
||||
};
|
||||
use util::maybe;
|
||||
|
||||
@@ -166,8 +166,7 @@ impl DiagnosticBlock {
|
||||
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
|
||||
let cx = &bcx.app;
|
||||
let status_colors = bcx.app.theme().status();
|
||||
|
||||
let max_width = bcx.em_width * 100.;
|
||||
let max_width = px(600.);
|
||||
|
||||
let (background_color, border_color) = match self.severity {
|
||||
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
|
||||
|
||||
@@ -46,8 +46,7 @@ use workspace::{
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct IncludeWarnings(bool);
|
||||
struct IncludeWarnings(bool);
|
||||
impl Global for IncludeWarnings {}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -380,6 +379,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| {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::*;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
DisplayPoint, InlayId,
|
||||
DisplayPoint,
|
||||
actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
|
||||
display_map::{DisplayRow, Inlay},
|
||||
display_map::DisplayRow,
|
||||
test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
|
||||
};
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
@@ -620,7 +620,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 20)]
|
||||
async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
@@ -779,162 +779,6 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
|
||||
}
|
||||
}
|
||||
|
||||
// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
|
||||
#[gpui::test]
|
||||
async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), json!({})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
|
||||
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
||||
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
|
||||
});
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
|
||||
});
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
|
||||
assert!(diagnostics.focus_handle.is_focused(window));
|
||||
});
|
||||
|
||||
let mut next_id = 0;
|
||||
let mut next_filename = 0;
|
||||
let mut language_server_ids = vec![LanguageServerId(0)];
|
||||
let mut updated_language_servers = HashSet::default();
|
||||
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
|
||||
Default::default();
|
||||
let mut next_inlay_id = 0;
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
// language server completes its diagnostic check
|
||||
0..=20 if !updated_language_servers.is_empty() => {
|
||||
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
|
||||
log::info!("finishing diagnostic check for language server {server_id}");
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.disk_based_diagnostics_finished(server_id, cx)
|
||||
});
|
||||
|
||||
if rng.gen_bool(0.5) {
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
if snapshot.buffer_snapshot.len() > 0 {
|
||||
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
|
||||
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
|
||||
log::info!(
|
||||
"adding inlay at {position}/{}: {:?}",
|
||||
snapshot.buffer_snapshot.len(),
|
||||
snapshot.buffer_snapshot.text(),
|
||||
);
|
||||
|
||||
editor.splice_inlays(
|
||||
&[],
|
||||
vec![Inlay {
|
||||
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
|
||||
position: snapshot.buffer_snapshot.anchor_before(position),
|
||||
text: Rope::from(format!("Test inlay {next_inlay_id}")),
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
// language server updates diagnostics
|
||||
_ => {
|
||||
let (path, server_id, diagnostics) =
|
||||
match current_diagnostics.iter_mut().choose(&mut rng) {
|
||||
// update existing set of diagnostics
|
||||
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
|
||||
(path.clone(), *server_id, diagnostics)
|
||||
}
|
||||
|
||||
// insert a set of diagnostics for a new path
|
||||
_ => {
|
||||
let path: PathBuf =
|
||||
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
|
||||
let len = rng.gen_range(128..256);
|
||||
let content =
|
||||
RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
fs.insert_file(&path, content.into_bytes()).await;
|
||||
|
||||
let server_id = match language_server_ids.iter().choose(&mut rng) {
|
||||
Some(server_id) if rng.gen_bool(0.5) => *server_id,
|
||||
_ => {
|
||||
let id = LanguageServerId(language_server_ids.len());
|
||||
language_server_ids.push(id);
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
path.clone(),
|
||||
server_id,
|
||||
current_diagnostics.entry((path, server_id)).or_default(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
updated_language_servers.insert(server_id);
|
||||
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
log::info!("updating diagnostics. language server {server_id} path {path:?}");
|
||||
randomly_update_diagnostics_for_path(
|
||||
&fs,
|
||||
&path,
|
||||
diagnostics,
|
||||
&mut next_id,
|
||||
&mut rng,
|
||||
);
|
||||
lsp_store
|
||||
.update_diagnostics(
|
||||
server_id,
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
|
||||
lsp::Url::parse("file:///test/fallback.rs").unwrap()
|
||||
}),
|
||||
diagnostics: diagnostics.clone(),
|
||||
version: None,
|
||||
},
|
||||
&[],
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
|
||||
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("updating mutated diagnostics view");
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.update_stale_excerpts(window, cx)
|
||||
});
|
||||
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -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_default_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
|
||||
|
||||
@@ -36,7 +36,7 @@ enum Transform {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Inlay {
|
||||
pub id: InlayId,
|
||||
pub(crate) id: InlayId,
|
||||
pub position: Anchor,
|
||||
pub text: text::Rope,
|
||||
}
|
||||
@@ -482,9 +482,6 @@ impl InlayMap {
|
||||
};
|
||||
|
||||
for inlay in &self.inlays[start_ix..] {
|
||||
if !inlay.position.is_valid(&buffer_snapshot) {
|
||||
continue;
|
||||
}
|
||||
let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
|
||||
if buffer_offset > buffer_edit.new.end {
|
||||
break;
|
||||
@@ -497,7 +494,9 @@ impl InlayMap {
|
||||
buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
|
||||
);
|
||||
|
||||
new_transforms.push(Transform::Inlay(inlay.clone()), &());
|
||||
if inlay.position.is_valid(&buffer_snapshot) {
|
||||
new_transforms.push(Transform::Inlay(inlay.clone()), &());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the rest of the edit.
|
||||
|
||||
@@ -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),
|
||||
@@ -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,
|
||||
@@ -17825,7 +17669,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 +18119,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) {
|
||||
@@ -1007,6 +1005,9 @@ impl EditorElement {
|
||||
} else {
|
||||
editor.hide_hovered_link(cx);
|
||||
hover_at(editor, None, window, cx);
|
||||
if gutter_hovered {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -271,12 +271,12 @@ fn main() {
|
||||
match judge_result {
|
||||
Ok(judge_output) => {
|
||||
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
|
||||
let score: u32 = judge_output.score;
|
||||
let score_index = (score.min(5)) as usize;
|
||||
|
||||
println!(
|
||||
"{} {}{}",
|
||||
SCORES[score_index], example.log_prefix, judge_output.score,
|
||||
SCORES[judge_output.score.min(5) as usize],
|
||||
example.log_prefix,
|
||||
judge_output.score,
|
||||
);
|
||||
judge_scores.push(judge_output.score);
|
||||
}
|
||||
@@ -304,6 +304,7 @@ fn main() {
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
// Flush telemetry events before exiting
|
||||
app_state.client.telemetry().flush_events();
|
||||
|
||||
cx.update(|cx| cx.quit())
|
||||
@@ -329,6 +330,7 @@ async fn run_example(
|
||||
for round in 0..judge_repetitions {
|
||||
let judge_result = example.judge(model.clone(), diff.clone(), round, cx).await;
|
||||
|
||||
// Log telemetry for this judge result
|
||||
if let Ok(judge_output) = &judge_result {
|
||||
let cohort_id = example
|
||||
.output_file_path
|
||||
@@ -337,9 +339,6 @@ async fn run_example(
|
||||
.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,
|
||||
@@ -354,8 +353,7 @@ async fn run_example(
|
||||
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
|
||||
diagnostics_summary = run_output.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
@@ -526,13 +524,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()
|
||||
})
|
||||
}
|
||||
|
||||
40
crates/evals/Cargo.toml
Normal file
40
crates/evals/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "evals"
|
||||
description = "Evaluations for Zed's AI features"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "eval"
|
||||
path = "src/eval.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
5
crates/evals/build.rs
Normal file
5
crates/evals/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
|
||||
}
|
||||
}
|
||||
718
crates/evals/src/eval.rs
Normal file
718
crates/evals/src/eval.rs
Normal file
@@ -0,0 +1,718 @@
|
||||
use ::fs::{Fs, RealFs};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use client::{Client, UserStore};
|
||||
use clock::RealSystemClock;
|
||||
use collections::BTreeMap;
|
||||
use dap::DapRegistry;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use open_ai::OpenAiEmbeddingModel;
|
||||
use project::Project;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use semantic_index::{
|
||||
EmbeddingProvider, OpenAiEmbeddingProvider, ProjectIndex, SemanticDb, Status,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use smol::Timer;
|
||||
use smol::channel::bounded;
|
||||
use smol::io::AsyncReadExt;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
process::{Stdio, exit},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
|
||||
const CODESEARCH_NET_DIR: &'static str = "target/datasets/code-search-net";
|
||||
const EVAL_REPOS_DIR: &'static str = "target/datasets/eval-repos";
|
||||
const EVAL_DB_PATH: &'static str = "target/eval_db";
|
||||
const SEARCH_RESULT_LIMIT: usize = 8;
|
||||
const SKIP_EVAL_PATH: &'static str = ".skip_eval";
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Commands {
|
||||
Fetch {},
|
||||
Run {
|
||||
#[arg(long)]
|
||||
repo: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct EvaluationProject {
|
||||
repo: String,
|
||||
sha: String,
|
||||
queries: Vec<EvaluationQuery>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct EvaluationQuery {
|
||||
query: String,
|
||||
expected_results: Vec<EvaluationSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
struct EvaluationSearchResult {
|
||||
file: String,
|
||||
lines: RangeInclusive<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct EvaluationProjectOutcome {
|
||||
repo: String,
|
||||
sha: String,
|
||||
queries: Vec<EvaluationQueryOutcome>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct EvaluationQueryOutcome {
|
||||
repo: String,
|
||||
query: String,
|
||||
expected_results: Vec<EvaluationSearchResult>,
|
||||
actual_results: Vec<EvaluationSearchResult>,
|
||||
covered_file_count: usize,
|
||||
overlapped_result_count: usize,
|
||||
covered_result_count: usize,
|
||||
total_result_count: usize,
|
||||
covered_result_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
env_logger::init();
|
||||
|
||||
gpui::Application::headless().run(move |cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
let client = Arc::new(ReqwestClient::user_agent("Zed LLM evals").unwrap());
|
||||
cx.set_http_client(client.clone());
|
||||
match cli.command {
|
||||
Commands::Fetch {} => {
|
||||
executor
|
||||
.clone()
|
||||
.spawn(async move {
|
||||
if let Err(err) = fetch_evaluation_resources(client, &executor).await {
|
||||
eprintln!("Error: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
exit(0);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Commands::Run { repo } => {
|
||||
cx.spawn(async move |cx| {
|
||||
if let Err(err) = run_evaluation(repo, &executor, cx).await {
|
||||
eprintln!("Error: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
exit(0);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_evaluation_resources(
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
executor: &BackgroundExecutor,
|
||||
) -> Result<()> {
|
||||
fetch_code_search_net_resources(&*http_client).await?;
|
||||
fetch_eval_repos(executor, &*http_client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_code_search_net_resources(http_client: &dyn HttpClient) -> Result<()> {
|
||||
eprintln!("Fetching CodeSearchNet evaluations...");
|
||||
|
||||
let annotations_url = "https://raw.githubusercontent.com/github/CodeSearchNet/master/resources/annotationStore.csv";
|
||||
|
||||
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
|
||||
fs::create_dir_all(&dataset_dir).expect("failed to create CodeSearchNet directory");
|
||||
|
||||
// Fetch the annotations CSV, which contains the human-annotated search relevances
|
||||
let annotations_path = dataset_dir.join("annotations.csv");
|
||||
let annotations_csv_content = if annotations_path.exists() {
|
||||
fs::read_to_string(&annotations_path).expect("failed to read annotations")
|
||||
} else {
|
||||
let response = http_client
|
||||
.get(annotations_url, Default::default(), true)
|
||||
.await
|
||||
.expect("failed to fetch annotations csv");
|
||||
let mut body = String::new();
|
||||
response
|
||||
.into_body()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.expect("failed to read annotations.csv response");
|
||||
fs::write(annotations_path, &body).expect("failed to write annotations.csv");
|
||||
body
|
||||
};
|
||||
|
||||
// Parse the annotations CSV. Skip over queries with zero relevance.
|
||||
let rows = annotations_csv_content.lines().filter_map(|line| {
|
||||
let mut values = line.split(',');
|
||||
let _language = values.next()?;
|
||||
let query = values.next()?;
|
||||
let github_url = values.next()?;
|
||||
let score = values.next()?;
|
||||
|
||||
if score == "0" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let url_path = github_url.strip_prefix("https://github.com/")?;
|
||||
let (url_path, hash) = url_path.split_once('#')?;
|
||||
let (repo_name, url_path) = url_path.split_once("/blob/")?;
|
||||
let (sha, file_path) = url_path.split_once('/')?;
|
||||
let line_range = if let Some((start, end)) = hash.split_once('-') {
|
||||
start.strip_prefix("L")?.parse::<u32>().ok()?..=end.strip_prefix("L")?.parse().ok()?
|
||||
} else {
|
||||
let row = hash.strip_prefix("L")?.parse().ok()?;
|
||||
row..=row
|
||||
};
|
||||
Some((repo_name, sha, query, file_path, line_range))
|
||||
});
|
||||
|
||||
// Group the annotations by repo and sha.
|
||||
let mut evaluations_by_repo = BTreeMap::new();
|
||||
for (repo_name, sha, query, file_path, lines) in rows {
|
||||
let evaluation_project = evaluations_by_repo
|
||||
.entry((repo_name, sha))
|
||||
.or_insert_with(|| EvaluationProject {
|
||||
repo: repo_name.to_string(),
|
||||
sha: sha.to_string(),
|
||||
queries: Vec::new(),
|
||||
});
|
||||
|
||||
let ix = evaluation_project
|
||||
.queries
|
||||
.iter()
|
||||
.position(|entry| entry.query == query)
|
||||
.unwrap_or_else(|| {
|
||||
evaluation_project.queries.push(EvaluationQuery {
|
||||
query: query.to_string(),
|
||||
expected_results: Vec::new(),
|
||||
});
|
||||
evaluation_project.queries.len() - 1
|
||||
});
|
||||
let results = &mut evaluation_project.queries[ix].expected_results;
|
||||
let result = EvaluationSearchResult {
|
||||
file: file_path.to_string(),
|
||||
lines,
|
||||
};
|
||||
if !results.contains(&result) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
let evaluations = evaluations_by_repo.into_values().collect::<Vec<_>>();
|
||||
let evaluations_path = dataset_dir.join("evaluations.json");
|
||||
fs::write(
|
||||
&evaluations_path,
|
||||
serde_json::to_vec_pretty(&evaluations).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Fetched CodeSearchNet evaluations into {}",
|
||||
evaluations_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Counts {
|
||||
covered_results: usize,
|
||||
overlapped_results: usize,
|
||||
covered_files: usize,
|
||||
total_results: usize,
|
||||
}
|
||||
|
||||
async fn run_evaluation(
|
||||
only_repo: Option<String>,
|
||||
executor: &BackgroundExecutor,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let mut http_client = None;
|
||||
cx.update(|cx| {
|
||||
let mut store = SettingsStore::new(cx);
|
||||
store
|
||||
.set_default_settings(settings::default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(store);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
http_client = Some(cx.http_client());
|
||||
cx.update_flags(false, vec![]);
|
||||
})
|
||||
.unwrap();
|
||||
let http_client = http_client.unwrap();
|
||||
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
|
||||
let evaluations_path = dataset_dir.join("evaluations.json");
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
let db_path = Path::new(EVAL_DB_PATH);
|
||||
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())) as Arc<dyn Fs>;
|
||||
let clock = Arc::new(RealSystemClock);
|
||||
let client = cx
|
||||
.update(|cx| {
|
||||
Client::new(
|
||||
clock,
|
||||
Arc::new(http_client::HttpClientWithUrl::new(
|
||||
http_client.clone(),
|
||||
"https://zed.dev",
|
||||
None,
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)).unwrap();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let evaluations = fs::read(&evaluations_path).expect("failed to read evaluations.json");
|
||||
let evaluations: Vec<EvaluationProject> = serde_json::from_slice(&evaluations).unwrap();
|
||||
|
||||
let embedding_provider = Arc::new(OpenAiEmbeddingProvider::new(
|
||||
http_client.clone(),
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
open_ai::OPEN_AI_API_URL.to_string(),
|
||||
api_key,
|
||||
));
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(executor.clone()));
|
||||
let debug_adapters = Arc::new(DapRegistry::default());
|
||||
cx.update(|cx| languages::init(language_registry.clone(), node_runtime.clone(), cx))
|
||||
.unwrap();
|
||||
|
||||
let mut counts = Counts::default();
|
||||
eprint!("Running evals.");
|
||||
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for evaluation_project in evaluations {
|
||||
if only_repo
|
||||
.as_ref()
|
||||
.map_or(false, |only_repo| only_repo != &evaluation_project.repo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
eprint!("\r\x1B[2K");
|
||||
eprint!(
|
||||
"Running evals. {}/{} covered. {}/{} overlapped. {}/{} files captured. Project: {}...",
|
||||
counts.covered_results,
|
||||
counts.total_results,
|
||||
counts.overlapped_results,
|
||||
counts.total_results,
|
||||
counts.covered_files,
|
||||
counts.total_results,
|
||||
evaluation_project.repo
|
||||
);
|
||||
|
||||
let repo_dir = repos_dir.join(&evaluation_project.repo);
|
||||
if !repo_dir.exists() || repo_dir.join(SKIP_EVAL_PATH).exists() {
|
||||
eprintln!("Skipping {}: directory not found", evaluation_project.repo);
|
||||
continue;
|
||||
}
|
||||
|
||||
let repo_db_path =
|
||||
db_path.join(format!("{}.db", evaluation_project.repo.replace('/', "_")));
|
||||
|
||||
let project = cx
|
||||
.update(|cx| {
|
||||
Project::local(
|
||||
client.clone(),
|
||||
node_runtime.clone(),
|
||||
user_store.clone(),
|
||||
language_registry.clone(),
|
||||
debug_adapters.clone(),
|
||||
fs.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let repo = evaluation_project.repo.clone();
|
||||
if let Err(err) = run_eval_project(
|
||||
evaluation_project,
|
||||
&user_store,
|
||||
repo_db_path,
|
||||
&repo_dir,
|
||||
&mut counts,
|
||||
project,
|
||||
embedding_provider.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("{repo} eval failed with error: {:?}", err);
|
||||
|
||||
failures.push((repo, err));
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"Running evals. {}/{} covered. {}/{} overlapped. {}/{} files captured. {} failed.",
|
||||
counts.covered_results,
|
||||
counts.total_results,
|
||||
counts.overlapped_results,
|
||||
counts.total_results,
|
||||
counts.covered_files,
|
||||
counts.total_results,
|
||||
failures.len(),
|
||||
);
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
eprintln!("Failures:\n");
|
||||
|
||||
for (index, (repo, failure)) in failures.iter().enumerate() {
|
||||
eprintln!("Failure #{} - {repo}\n{:?}", index + 1, failure);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Some evals failed."))
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_eval_project(
|
||||
evaluation_project: EvaluationProject,
|
||||
user_store: &Entity<UserStore>,
|
||||
repo_db_path: PathBuf,
|
||||
repo_dir: &Path,
|
||||
counts: &mut Counts,
|
||||
project: Entity<Project>,
|
||||
embedding_provider: Arc<dyn EmbeddingProvider>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut semantic_index = SemanticDb::new(repo_db_path, embedding_provider, cx).await?;
|
||||
|
||||
let (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(repo_dir, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
worktree
|
||||
.update(cx, |worktree, _| {
|
||||
worktree.as_local().unwrap().scan_complete()
|
||||
})?
|
||||
.await;
|
||||
|
||||
let project_index = cx.update(|cx| semantic_index.create_project_index(project.clone(), cx))?;
|
||||
wait_for_indexing_complete(&project_index, cx, Some(Duration::from_secs(120))).await;
|
||||
|
||||
for query in evaluation_project.queries {
|
||||
let results = {
|
||||
// Retry search up to 3 times in case of timeout, network failure, etc.
|
||||
let mut retries_remaining = 3;
|
||||
let mut result;
|
||||
|
||||
loop {
|
||||
match cx.update(|cx| {
|
||||
let project_index = project_index.read(cx);
|
||||
project_index.search(vec![query.query.clone()], SEARCH_RESULT_LIMIT, cx)
|
||||
}) {
|
||||
Ok(task) => match task.await {
|
||||
Ok(answer) => {
|
||||
result = Ok(answer);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
result = Err(err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
result = Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
if retries_remaining > 0 {
|
||||
eprintln!(
|
||||
"Retrying search after it failed on query {:?} with {:?}",
|
||||
query, result
|
||||
);
|
||||
retries_remaining -= 1;
|
||||
} else {
|
||||
eprintln!(
|
||||
"Ran out of retries; giving up on search which failed on query {:?} with {:?}",
|
||||
query, result
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SemanticDb::load_results(result?, &fs.clone(), &cx).await?
|
||||
};
|
||||
|
||||
let mut project_covered_result_count = 0;
|
||||
let mut project_overlapped_result_count = 0;
|
||||
let mut project_covered_file_count = 0;
|
||||
let mut covered_result_indices = Vec::new();
|
||||
for expected_result in &query.expected_results {
|
||||
let mut file_matched = false;
|
||||
let mut range_overlapped = false;
|
||||
let mut range_covered = false;
|
||||
|
||||
for (ix, result) in results.iter().enumerate() {
|
||||
if result.path.as_ref() == Path::new(&expected_result.file) {
|
||||
file_matched = true;
|
||||
let start_matched = result.row_range.contains(expected_result.lines.start());
|
||||
let end_matched = result.row_range.contains(expected_result.lines.end());
|
||||
|
||||
if start_matched || end_matched {
|
||||
range_overlapped = true;
|
||||
}
|
||||
|
||||
if start_matched && end_matched {
|
||||
range_covered = true;
|
||||
covered_result_indices.push(ix);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if range_covered {
|
||||
project_covered_result_count += 1
|
||||
};
|
||||
if range_overlapped {
|
||||
project_overlapped_result_count += 1
|
||||
};
|
||||
if file_matched {
|
||||
project_covered_file_count += 1
|
||||
};
|
||||
}
|
||||
let outcome_repo = evaluation_project.repo.clone();
|
||||
|
||||
let query_results = EvaluationQueryOutcome {
|
||||
repo: outcome_repo,
|
||||
query: query.query,
|
||||
total_result_count: query.expected_results.len(),
|
||||
covered_result_count: project_covered_result_count,
|
||||
overlapped_result_count: project_overlapped_result_count,
|
||||
covered_file_count: project_covered_file_count,
|
||||
expected_results: query.expected_results,
|
||||
actual_results: results
|
||||
.iter()
|
||||
.map(|result| EvaluationSearchResult {
|
||||
file: result.path.to_string_lossy().to_string(),
|
||||
lines: result.row_range.clone(),
|
||||
})
|
||||
.collect(),
|
||||
covered_result_indices,
|
||||
};
|
||||
|
||||
counts.overlapped_results += query_results.overlapped_result_count;
|
||||
counts.covered_results += query_results.covered_result_count;
|
||||
counts.covered_files += query_results.covered_file_count;
|
||||
counts.total_results += query_results.total_result_count;
|
||||
|
||||
println!("{}", serde_json::to_string(&query_results)?);
|
||||
}
|
||||
|
||||
user_store.update(cx, |_, _| {
|
||||
drop(semantic_index);
|
||||
drop(project);
|
||||
drop(worktree);
|
||||
drop(project_index);
|
||||
})
|
||||
}
|
||||
|
||||
async fn wait_for_indexing_complete(
|
||||
project_index: &Entity<ProjectIndex>,
|
||||
cx: &mut AsyncApp,
|
||||
timeout: Option<Duration>,
|
||||
) {
|
||||
let (tx, rx) = bounded(1);
|
||||
let subscription = cx.update(|cx| {
|
||||
cx.subscribe(project_index, move |_, event, _| {
|
||||
if let Status::Idle = event {
|
||||
let _ = tx.try_send(*event);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let result = match timeout {
|
||||
Some(timeout_duration) => {
|
||||
smol::future::or(
|
||||
async {
|
||||
rx.recv().await.map_err(|_| ())?;
|
||||
Ok(())
|
||||
},
|
||||
async {
|
||||
Timer::after(timeout_duration).await;
|
||||
Err(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => rx.recv().await.map(|_| ()).map_err(|_| ()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
if let Some(timeout) = timeout {
|
||||
eprintln!("Timeout: Indexing did not complete within {:?}", timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(subscription);
|
||||
}
|
||||
|
||||
async fn fetch_eval_repos(
|
||||
executor: &BackgroundExecutor,
|
||||
http_client: &dyn HttpClient,
|
||||
) -> Result<()> {
|
||||
let dataset_dir = Path::new(CODESEARCH_NET_DIR);
|
||||
let evaluations_path = dataset_dir.join("evaluations.json");
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
|
||||
let evaluations = fs::read(&evaluations_path).expect("failed to read evaluations.json");
|
||||
let evaluations: Vec<EvaluationProject> = serde_json::from_slice(&evaluations).unwrap();
|
||||
|
||||
eprintln!("Fetching evaluation repositories...");
|
||||
|
||||
executor
|
||||
.scoped(move |scope| {
|
||||
let done_count = Arc::new(AtomicUsize::new(0));
|
||||
let len = evaluations.len();
|
||||
for chunk in evaluations.chunks(evaluations.len() / 8) {
|
||||
let chunk = chunk.to_vec();
|
||||
let done_count = done_count.clone();
|
||||
scope.spawn(async move {
|
||||
for EvaluationProject { repo, sha, .. } in chunk {
|
||||
eprint!(
|
||||
"\rFetching evaluation repositories ({}/{})...",
|
||||
done_count.load(SeqCst),
|
||||
len,
|
||||
);
|
||||
|
||||
fetch_eval_repo(repo, sha, repos_dir, http_client).await;
|
||||
done_count.fetch_add(1, SeqCst);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_eval_repo(
|
||||
repo: String,
|
||||
sha: String,
|
||||
repos_dir: &Path,
|
||||
http_client: &dyn HttpClient,
|
||||
) {
|
||||
let Some((owner, repo_name)) = repo.split_once('/') else {
|
||||
return;
|
||||
};
|
||||
let repo_dir = repos_dir.join(owner).join(repo_name);
|
||||
fs::create_dir_all(&repo_dir).unwrap();
|
||||
let skip_eval_path = repo_dir.join(SKIP_EVAL_PATH);
|
||||
if skip_eval_path.exists() {
|
||||
return;
|
||||
}
|
||||
if let Ok(head_content) = fs::read_to_string(&repo_dir.join(".git").join("HEAD")) {
|
||||
if head_content.trim() == sha {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let repo_response = http_client
|
||||
.send(
|
||||
http_client::Request::builder()
|
||||
.method(Method::HEAD)
|
||||
.uri(format!("https://github.com/{}", repo))
|
||||
.body(Default::default())
|
||||
.expect(""),
|
||||
)
|
||||
.await
|
||||
.expect("failed to check github repo");
|
||||
if !repo_response.status().is_success() && !repo_response.status().is_redirection() {
|
||||
fs::write(&skip_eval_path, "").unwrap();
|
||||
eprintln!(
|
||||
"Repo {repo} is no longer public ({:?}). Skipping",
|
||||
repo_response.status()
|
||||
);
|
||||
return;
|
||||
}
|
||||
if !repo_dir.join(".git").exists() {
|
||||
let init_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["init"])
|
||||
.output()
|
||||
.unwrap();
|
||||
if !init_output.status.success() {
|
||||
eprintln!(
|
||||
"Failed to initialize git repository for {}: {}",
|
||||
repo,
|
||||
String::from_utf8_lossy(&init_output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let url = format!("https://github.com/{}.git", repo);
|
||||
util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["remote", "add", "-f", "origin", &url])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
let fetch_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["fetch", "--depth", "1", "origin", &sha])
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
if !fetch_output.status.success() {
|
||||
eprintln!(
|
||||
"Failed to fetch {} for {}: {}",
|
||||
sha,
|
||||
repo,
|
||||
String::from_utf8_lossy(&fetch_output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let checkout_output = util::command::new_std_command("git")
|
||||
.current_dir(&repo_dir)
|
||||
.args(&["checkout", &sha])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
if !checkout_output.status.success() {
|
||||
eprintln!(
|
||||
"Failed to checkout {} for {}: {}",
|
||||
sha,
|
||||
repo,
|
||||
String::from_utf8_lossy(&checkout_output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -599,33 +599,11 @@ 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 Some(ix) = binary_search(
|
||||
conflicted_start,
|
||||
conflicted_start + self.conflicted_count,
|
||||
|ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
},
|
||||
) {
|
||||
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))
|
||||
{
|
||||
return Some(ix);
|
||||
}
|
||||
}
|
||||
@@ -635,14 +613,8 @@ impl GitPanel {
|
||||
} else {
|
||||
0
|
||||
} + 1;
|
||||
if let Some(ix) =
|
||||
binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
})
|
||||
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))
|
||||
{
|
||||
return Some(ix);
|
||||
}
|
||||
@@ -657,14 +629,8 @@ impl GitPanel {
|
||||
} else {
|
||||
0
|
||||
} + 1;
|
||||
if let Some(ix) =
|
||||
binary_search(untracked_start, untracked_start + self.new_count, |ix| {
|
||||
self.entries[ix]
|
||||
.status_entry()
|
||||
.unwrap()
|
||||
.repo_path
|
||||
.cmp(&path)
|
||||
})
|
||||
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))
|
||||
{
|
||||
return Some(ix);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ pub struct GenerateContentRequest {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub model: String,
|
||||
pub contents: Vec<Content>,
|
||||
pub system_instruction: Option<SystemInstruction>,
|
||||
pub system_instructions: Option<SystemInstructions>,
|
||||
pub generation_config: Option<GenerationConfig>,
|
||||
pub safety_settings: Option<Vec<SafetySetting>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -162,7 +162,7 @@ pub struct Content {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemInstruction {
|
||||
pub struct SystemInstructions {
|
||||
pub parts: Vec<Part>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -4,7 +4,7 @@ use credentials_provider::CredentialsProvider;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
|
||||
use google_ai::{
|
||||
FunctionDeclaration, GenerateContentResponse, Part, SystemInstruction, UsageMetadata,
|
||||
FunctionDeclaration, GenerateContentResponse, Part, SystemInstructions, UsageMetadata,
|
||||
};
|
||||
use gpui::{
|
||||
AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
|
||||
@@ -405,7 +405,7 @@ pub fn into_google(
|
||||
.map_or(false, |msg| matches!(msg.role, Role::System))
|
||||
{
|
||||
let message = request.messages.remove(0);
|
||||
Some(SystemInstruction {
|
||||
Some(SystemInstructions {
|
||||
parts: map_content(message.content),
|
||||
})
|
||||
} else {
|
||||
@@ -414,7 +414,7 @@ pub fn into_google(
|
||||
|
||||
google_ai::GenerateContentRequest {
|
||||
model,
|
||||
system_instruction: system_instructions,
|
||||
system_instructions,
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Anchor {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -792,48 +792,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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3094,9 +3094,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,7 +23,6 @@ test-support = ["fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-watch.workspace = true
|
||||
backtrace = "0.3"
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -8,10 +8,6 @@ use std::path::PathBuf;
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
|
||||
/// by having Zed act like netcat communicating over a Unix socket.
|
||||
#[arg(long, hide = true)]
|
||||
askpass: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -50,11 +46,6 @@ fn main() {
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
if let Some(socket_path) = &cli.askpass {
|
||||
askpass::main(socket_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = match cli.command {
|
||||
Some(Commands::Run {
|
||||
log_file,
|
||||
|
||||
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;
|
||||
|
||||
@@ -23,8 +23,3 @@ snippet.workspace = true
|
||||
util.workspace = true
|
||||
schemars.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -222,15 +222,15 @@ impl SnippetProvider {
|
||||
.lookup_snippets::<false>(language, cx),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(registry) = SnippetRegistry::try_global(cx) else {
|
||||
return user_snippets;
|
||||
};
|
||||
|
||||
let registry_snippets = registry.get_snippets(language);
|
||||
user_snippets.extend(registry_snippets);
|
||||
}
|
||||
|
||||
let Some(registry) = SnippetRegistry::try_global(cx) else {
|
||||
return user_snippets;
|
||||
};
|
||||
|
||||
let registry_snippets = registry.get_snippets(language);
|
||||
user_snippets.extend(registry_snippets);
|
||||
|
||||
user_snippets
|
||||
}
|
||||
|
||||
@@ -244,38 +244,3 @@ impl SnippetProvider {
|
||||
requested_snippets
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_lookup_snippets_dup_registry_snippets(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
cx.update(|cx| {
|
||||
SnippetRegistry::init_global(cx);
|
||||
SnippetRegistry::global(cx)
|
||||
.register_snippets(
|
||||
"ruby".as_ref(),
|
||||
indoc! {r#"
|
||||
{
|
||||
"Log to console": {
|
||||
"prefix": "log",
|
||||
"body": ["console.info(\"Hello, ${1:World}!\")", "$0"],
|
||||
"description": "Logs to console"
|
||||
}
|
||||
}
|
||||
"#},
|
||||
)
|
||||
.unwrap();
|
||||
let provider = SnippetProvider::new(fs.clone(), Default::default(), cx);
|
||||
cx.update_entity(&provider, |provider, cx| {
|
||||
assert_eq!(1, provider.snippets_for(Some("ruby".to_owned()), cx).len());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -110,7 +110,11 @@ impl RenderOnce for Banner {
|
||||
|
||||
let mut container = base.bg(bg_color).border_color(border_color);
|
||||
|
||||
let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll();
|
||||
let mut content_area = h_flex()
|
||||
.id("content_area")
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.overflow_x_scroll();
|
||||
|
||||
if self.icon.is_none() {
|
||||
content_area =
|
||||
@@ -126,10 +130,13 @@ impl RenderOnce for Banner {
|
||||
.pl_2()
|
||||
.pr_0p5()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(content_area)
|
||||
.child(action_slot);
|
||||
} else {
|
||||
container = container.px_2().child(div().w_full().child(content_area));
|
||||
container = container
|
||||
.px_2()
|
||||
.child(div().w_full().flex_1().child(content_area));
|
||||
}
|
||||
|
||||
container
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "web_search"
|
||||
name = "ui_parking_lot"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -9,12 +9,16 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/web_search.rs"
|
||||
path = "src/ui_parking_lot.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
linkme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/ui_parking_lot/src/agent/mod.rs
Normal file
1
crates/ui_parking_lot/src/agent/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod usage_banner;
|
||||
475
crates/ui_parking_lot/src/agent/usage_banner.rs
Normal file
475
crates/ui_parking_lot/src/agent/usage_banner.rs
Normal file
@@ -0,0 +1,475 @@
|
||||
use gpui::Entity;
|
||||
use ui::{Banner, Severity};
|
||||
use ui::{ProgressBar, prelude::*};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum CurrentPlan {
|
||||
Trial,
|
||||
Free,
|
||||
Paid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum CapReason {
|
||||
RequestLimit,
|
||||
SpendLimit,
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
current_plan: CurrentPlan,
|
||||
current_requests: u32,
|
||||
current_spend: u32,
|
||||
monthly_cap: u32,
|
||||
usage_based_enabled: bool,
|
||||
usage_progress: Entity<ProgressBar>,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
/// Creates a new UsageBanner with the provided values
|
||||
pub fn new(
|
||||
current_plan: CurrentPlan,
|
||||
current_requests: u32,
|
||||
current_spend: u32,
|
||||
monthly_cap: u32,
|
||||
usage_based_enabled: bool,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let usage_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"usage_progress",
|
||||
current_requests as f32,
|
||||
request_cap_for_plan(¤t_plan) as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let banner = Self {
|
||||
current_plan,
|
||||
current_requests,
|
||||
current_spend,
|
||||
monthly_cap,
|
||||
usage_based_enabled,
|
||||
usage_progress,
|
||||
};
|
||||
|
||||
// No need to update styling here as it will be done when rendering
|
||||
banner
|
||||
}
|
||||
|
||||
/// Returns the request cap based on the current plan
|
||||
pub fn request_cap(&self) -> u32 {
|
||||
request_cap_for_plan(&self.current_plan)
|
||||
}
|
||||
|
||||
/// Check if the user is capped due to hitting request limits
|
||||
pub fn is_capped_by_requests(&self) -> bool {
|
||||
self.current_requests >= self.request_cap()
|
||||
}
|
||||
|
||||
/// Check if the user is capped due to hitting spend limits
|
||||
pub fn is_capped_by_spend(&self) -> bool {
|
||||
// Only check spend limit if spending is enabled and cap is set
|
||||
self.usage_based_enabled && self.monthly_cap > 0 && self.current_spend >= self.monthly_cap
|
||||
}
|
||||
|
||||
/// Check if the user is approaching request limit (>=90%)
|
||||
pub fn is_approaching_request_limit(&self) -> bool {
|
||||
let threshold = (self.request_cap() as f32 * 0.9) as u32;
|
||||
self.current_requests >= threshold && self.current_requests < self.request_cap()
|
||||
}
|
||||
|
||||
/// Check if the user is approaching spend limit (>=90%)
|
||||
pub fn is_approaching_spend_limit(&self) -> bool {
|
||||
// Only check if spending is enabled and cap is set
|
||||
self.usage_based_enabled
|
||||
&& self.monthly_cap > 0
|
||||
&& self.current_spend >= (self.monthly_cap as f32 * 0.9) as u32
|
||||
&& self.current_spend < self.monthly_cap
|
||||
}
|
||||
|
||||
/// Check if the user is capped and returns the reason
|
||||
pub fn cap_status(&self) -> Option<CapReason> {
|
||||
if self.is_capped_by_requests() {
|
||||
Some(CapReason::RequestLimit)
|
||||
} else if self.is_capped_by_spend() {
|
||||
Some(CapReason::SpendLimit)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the user is capped for any reason
|
||||
pub fn is_capped(&self) -> bool {
|
||||
matches!(
|
||||
self.cap_status(),
|
||||
Some(CapReason::RequestLimit | CapReason::SpendLimit)
|
||||
)
|
||||
}
|
||||
|
||||
/// Update the current request count and progress bar
|
||||
pub fn update_requests(&mut self, requests: u32, cx: &mut Context<Self>) {
|
||||
self.current_requests = requests;
|
||||
self.update_progress_bar(cx);
|
||||
self.update_progress_styling(cx);
|
||||
}
|
||||
|
||||
/// Update the current spend amount
|
||||
pub fn update_spend(&mut self, spend: u32, cx: &mut Context<Self>) {
|
||||
self.current_spend = spend;
|
||||
self.update_progress_bar(cx);
|
||||
self.update_progress_styling(cx);
|
||||
}
|
||||
|
||||
/// Update the progress bar styling based on current usage levels
|
||||
fn update_progress_styling(&self, cx: &mut Context<Self>) {
|
||||
let is_near_cap = self.current_requests as f32 >= self.request_cap() as f32 * 0.9;
|
||||
let is_capped = self.is_capped();
|
||||
|
||||
self.usage_progress.update(cx, |progress_bar, cx| {
|
||||
if is_capped {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
} else if is_near_cap {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
} else {
|
||||
progress_bar.fg_color(cx.theme().status().info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn should_show_request_progress(&self) -> bool {
|
||||
// Show request progress for all plans as long as not capped
|
||||
// Only show if we have a non-zero request cap
|
||||
self.request_cap() > 0 && !self.is_capped_by_requests() && !self.is_capped_by_spend()
|
||||
}
|
||||
|
||||
/// Show the spend progress bar once requests are capped
|
||||
/// if the user has usage based enabled
|
||||
fn should_show_spend_progress(&self) -> bool {
|
||||
// Only show spend progress for paid plans with usage-based pricing enabled
|
||||
// and when a monthly cap is set
|
||||
self.current_plan == CurrentPlan::Paid
|
||||
&& self.usage_based_enabled
|
||||
&& self.monthly_cap > 0
|
||||
&& self.is_capped_by_requests()
|
||||
&& !self.is_capped_by_spend()
|
||||
}
|
||||
|
||||
/// Update the progress bar with current values
|
||||
fn update_progress_bar(&mut self, cx: &mut Context<Self>) {
|
||||
// Update the progress bar with new values
|
||||
// We need to recreate it to update both value and max
|
||||
self.usage_progress.update(cx, |progress_bar, cx| {
|
||||
// Update progress bar value
|
||||
*progress_bar = ProgressBar::new(
|
||||
"usage_progress",
|
||||
self.current_requests as f32,
|
||||
self.request_cap() as f32,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
if self.is_capped_by_spend() || self.is_capped_by_requests() {
|
||||
return Severity::Error;
|
||||
}
|
||||
|
||||
if self.is_approaching_request_limit() || self.is_approaching_spend_limit() {
|
||||
return Severity::Warning;
|
||||
}
|
||||
|
||||
Severity::Info
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to get the request cap based on plan type
|
||||
fn request_cap_for_plan(plan: &CurrentPlan) -> u32 {
|
||||
match plan {
|
||||
CurrentPlan::Trial => 150,
|
||||
CurrentPlan::Free => 50,
|
||||
CurrentPlan::Paid => 500,
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for UsageBanner {
|
||||
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let formatted_requests = format!("{} / {}", self.current_requests, self.request_cap());
|
||||
let formatted_spend = format!(
|
||||
"${:.2} / ${:.2}",
|
||||
self.current_spend as f32 / 100.0,
|
||||
if self.monthly_cap > 0 {
|
||||
self.monthly_cap as f32 / 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
);
|
||||
|
||||
let (message, action_button) = if self.is_capped_by_spend() {
|
||||
(
|
||||
"Monthly spending limit reached",
|
||||
Some(Button::new("manage", "Manage Spending").into_any_element()),
|
||||
)
|
||||
} else if self.is_capped_by_requests() {
|
||||
let msg = match self.current_plan {
|
||||
CurrentPlan::Trial => "Trial request limit reached",
|
||||
CurrentPlan::Free => "Free tier request limit reached",
|
||||
CurrentPlan::Paid => "Monthly request limit reached",
|
||||
};
|
||||
|
||||
let action = match self.current_plan {
|
||||
CurrentPlan::Trial | CurrentPlan::Free => {
|
||||
Some(Button::new("upgrade", "Upgrade").into_any_element())
|
||||
}
|
||||
CurrentPlan::Paid => {
|
||||
if self.usage_based_enabled {
|
||||
Some(Button::new("manage", "Manage").into_any_element())
|
||||
} else {
|
||||
Some(Button::new("enable-usage", "Try Usaged-Based").into_any_element())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(msg, action)
|
||||
} else if self.is_approaching_request_limit() {
|
||||
let msg = "Approaching request limit";
|
||||
|
||||
let action = match self.current_plan {
|
||||
CurrentPlan::Trial | CurrentPlan::Free => {
|
||||
Some(Button::new("upgrade", "Upgrade").into_any_element())
|
||||
}
|
||||
CurrentPlan::Paid => {
|
||||
if !self.usage_based_enabled {
|
||||
Some(Button::new("enable-usage", "Manage").into_any_element())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(msg, action)
|
||||
} else if self.is_approaching_spend_limit() {
|
||||
(
|
||||
"Approaching monthly spend limit",
|
||||
Some(Button::new("manage", "Manage Spending").into_any_element()),
|
||||
)
|
||||
} else {
|
||||
let msg = match self.current_plan {
|
||||
CurrentPlan::Trial => "Zed AI Trial",
|
||||
CurrentPlan::Free => "Zed AI Free",
|
||||
CurrentPlan::Paid => "Zed AI Paid",
|
||||
};
|
||||
|
||||
(msg, None)
|
||||
};
|
||||
|
||||
// Build the content section with usage information
|
||||
let mut content = h_flex().flex_1().gap_1().child(Label::new(message));
|
||||
|
||||
// Add usage progress section if we should show it
|
||||
if self.should_show_request_progress() {
|
||||
content = content.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.max_w(px(180.))
|
||||
.child(self.usage_progress.clone()),
|
||||
)
|
||||
.child(
|
||||
Label::new(formatted_requests)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add spending information for Paid users with usage-based pricing
|
||||
if self.should_show_spend_progress() {
|
||||
content = content.child(
|
||||
h_flex().flex_1().justify_end().gap_1p5().child(
|
||||
Label::new(formatted_spend)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Create the banner with appropriate severity and content
|
||||
let mut banner = Banner::new().severity(self.severity()).children(content);
|
||||
|
||||
// Add action button if available
|
||||
if let Some(action) = action_button {
|
||||
banner = banner.action_slot(action);
|
||||
}
|
||||
|
||||
banner
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UsageBanner {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Notification
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
// Create instances of UsageBanner for different scenarios
|
||||
// Trial plan examples (cap = 150)
|
||||
let new_trial_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 10, 0, 0, false, cx));
|
||||
let trial_user_warning =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 135, 0, 0, false, cx));
|
||||
let trial_user_capped =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Trial, 150, 0, 0, false, cx));
|
||||
|
||||
// Free plan examples (cap = 50)
|
||||
let free_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 25, 0, 0, false, cx));
|
||||
let free_user_warning =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 45, 0, 0, false, cx));
|
||||
let free_user_capped =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Free, 50, 0, 0, false, cx));
|
||||
|
||||
// Pro plan examples without usage-based pricing (cap = 500)
|
||||
let paid_user = cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 250, 0, 0, false, cx));
|
||||
let paid_user_warning =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 450, 0, 0, false, cx));
|
||||
let paid_user_capped =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 0, 0, false, cx));
|
||||
|
||||
// Pro plan examples with usage-based pricing and monthly spend cap (cap = 500)
|
||||
let paid_user_usage_based =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 5000, 20000, true, cx));
|
||||
let paid_user_usage_based_warning =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 18000, 20000, true, cx));
|
||||
let paid_user_usage_based_capped =
|
||||
cx.new(|cx| UsageBanner::new(CurrentPlan::Paid, 500, 20000, 20000, true, cx));
|
||||
|
||||
// Group examples by plan type
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(new_trial_user.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(trial_user_warning.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(trial_user_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_user.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_user_warning.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_user_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let paid_examples = vec![
|
||||
single_example(
|
||||
"Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user_warning.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let paid_usage_based_examples = vec![
|
||||
single_example(
|
||||
"Pro with UBP - After Request Cap",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user_usage_based.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Pro with UBP - Approaching Spend Cap",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user_usage_based_warning.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Pro with UBP - Spend Cap Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(paid_user_usage_based_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
// Combine all examples
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![
|
||||
Label::new("Trial Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(trial_examples).vertical().into_any_element(),
|
||||
Label::new("Free Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(free_examples).vertical().into_any_element(),
|
||||
Label::new("Pro Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(paid_examples).vertical().into_any_element(),
|
||||
Label::new("Pro Plan with Usage-Based Pricing")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(paid_usage_based_examples)
|
||||
.vertical()
|
||||
.into_any_element(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
8
crates/ui_parking_lot/src/ui_parking_lot.rs
Normal file
8
crates/ui_parking_lot/src/ui_parking_lot.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! # UI Parking Lot
|
||||
//!
|
||||
//! A place for engineering-ready components to be parked
|
||||
//! until someone has the time to pick them up and implement them further.
|
||||
|
||||
mod agent;
|
||||
|
||||
pub use agent::usage_banner::UsageBanner;
|
||||
@@ -1,4 +1,6 @@
|
||||
use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement, scroll::Autoscroll};
|
||||
use editor::{
|
||||
Anchor, Bias, Direction, Editor, display_map::ToDisplayPoint, movement, scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{Context, Window, actions};
|
||||
|
||||
use crate::{Vim, state::Mode};
|
||||
@@ -23,60 +25,68 @@ impl Vim {
|
||||
) {
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
if self.change_list.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let prev = self.change_list_position.unwrap_or(self.change_list.len());
|
||||
let next = if direction == Direction::Prev {
|
||||
prev.saturating_sub(count)
|
||||
} else {
|
||||
(prev + count).min(self.change_list.len() - 1)
|
||||
};
|
||||
self.change_list_position = Some(next);
|
||||
let Some(selections) = self.change_list.get(next).cloned() else {
|
||||
return;
|
||||
};
|
||||
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||
if let Some(selections) = editor
|
||||
.change_list
|
||||
.next_change(count, direction)
|
||||
.map(|s| s.to_vec())
|
||||
{
|
||||
editor.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
|
||||
}))
|
||||
})
|
||||
};
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let map = s.display_map();
|
||||
s.select_display_ranges(selections.into_iter().map(|a| {
|
||||
let point = a.to_display_point(&map);
|
||||
point..point
|
||||
}))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some((new_positions, buffer)) = self.update_editor(window, cx, |vim, editor, _, cx| {
|
||||
let Some((map, selections, buffer)) = self.update_editor(window, cx, |_, editor, _, cx| {
|
||||
let (map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
let buffer = editor.buffer().clone();
|
||||
|
||||
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| {
|
||||
let point = if vim.mode == Mode::Insert {
|
||||
movement::saturating_left(&map, s.head())
|
||||
} else {
|
||||
s.head()
|
||||
};
|
||||
map.display_point_to_anchor(point, Bias::Left)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor
|
||||
.change_list
|
||||
.push_to_change_list(pop_state, new_positions.clone());
|
||||
|
||||
(new_positions, buffer)
|
||||
(map, selections, buffer)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pop_state = self
|
||||
.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: Vec<Anchor> = selections
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let point = if self.mode == Mode::Insert {
|
||||
movement::saturating_left(&map, s.head())
|
||||
} else {
|
||||
s.head()
|
||||
};
|
||||
map.display_point_to_anchor(point, Bias::Left)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.change_list_position.take();
|
||||
if pop_state {
|
||||
self.change_list.pop();
|
||||
}
|
||||
self.change_list.push(new_positions.clone());
|
||||
self.set_mark(".".to_string(), new_positions, &buffer, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,15 +132,7 @@ fn scroll_editor(
|
||||
let max_visible_row = top.row().0.saturating_add(
|
||||
(visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin),
|
||||
);
|
||||
// scroll off the end.
|
||||
let max_row = if top.row().0 + visible_line_count as u32 >= map.max_point().row().0 {
|
||||
map.max_point().row()
|
||||
} else {
|
||||
DisplayRow(
|
||||
(top.row().0 + visible_line_count as u32)
|
||||
.saturating_sub(1 + vertical_scroll_margin),
|
||||
)
|
||||
};
|
||||
let max_row = DisplayRow(map.max_point().row().0.max(max_visible_row));
|
||||
|
||||
let new_row = if full_page_up {
|
||||
// Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems
|
||||
@@ -380,14 +372,14 @@ mod test {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
cx.neovim.set_option(&format!("scrolloff={}", 0)).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |s| {
|
||||
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
|
||||
// s.vertical_scroll_margin = Some(0.);
|
||||
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -403,24 +395,4 @@ mod test {
|
||||
cx.simulate_shared_keystrokes("ctrl-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
for _ in 0..8 {
|
||||
cx.simulate_shared_keystrokes("ctrl-e").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
for _ in 0..8 {
|
||||
cx.simulate_shared_keystrokes("ctrl-y").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user