Compare commits
75 Commits
xcode-styl
...
tweak-wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f8610f36a | ||
|
|
faca128304 | ||
|
|
3f0f316f4d | ||
|
|
9d6b2e8a32 | ||
|
|
1d74fdc59f | ||
|
|
b7bfdd3383 | ||
|
|
0e2e5b8b0d | ||
|
|
6b06685723 | ||
|
|
8d894dd1df | ||
|
|
0eee768e7b | ||
|
|
f1f19a32fb | ||
|
|
2ff155d5a2 | ||
|
|
eb74df632b | ||
|
|
0068de0386 | ||
|
|
a11647d07f | ||
|
|
274f2e90da | ||
|
|
31b7786be7 | ||
|
|
351ba5023b | ||
|
|
3041de0cdf | ||
|
|
52c42125a7 | ||
|
|
62e8f45304 | ||
|
|
0fe73a99e5 | ||
|
|
6e9c6c5684 | ||
|
|
42f788185a | ||
|
|
a5b2428897 | ||
|
|
0629804390 | ||
|
|
3151b5efc1 | ||
|
|
782fbfad90 | ||
|
|
2caa19214b | ||
|
|
bff5d85ff4 | ||
|
|
abe5d523e1 | ||
|
|
8fb3199a84 | ||
|
|
0d809c21ba | ||
|
|
93b1e95a5d | ||
|
|
49bc2e61da | ||
|
|
9a4bcd11a2 | ||
|
|
2ee5bedfa9 | ||
|
|
d497f52e17 | ||
|
|
f022a13091 | ||
|
|
c74ecb4654 | ||
|
|
7609ca7a8d | ||
|
|
32906bfa7c | ||
|
|
5fafab6e52 | ||
|
|
a2e786e0f9 | ||
|
|
b0086b472f | ||
|
|
d10cc13924 | ||
|
|
2680a78f9c | ||
|
|
197828980c | ||
|
|
7c4da37322 | ||
|
|
ce164f5e65 | ||
|
|
42c59014a9 | ||
|
|
3db452eec7 | ||
|
|
6e77e8405b | ||
|
|
465f64da7e | ||
|
|
e5a8cc7aab | ||
|
|
bdf29bf76f | ||
|
|
402c61c00d | ||
|
|
59e88ce82b | ||
|
|
22ab4c53d1 | ||
|
|
f106ea7641 | ||
|
|
e37ef2a991 | ||
|
|
1c05062482 | ||
|
|
8c04f12499 | ||
|
|
aa7ccecc49 | ||
|
|
f4aeeda2d9 | ||
|
|
ca0bd53bed | ||
|
|
ae6237178c | ||
|
|
ac3328adb6 | ||
|
|
d63909c598 | ||
|
|
c3d0230f89 | ||
|
|
bc5927d5af | ||
|
|
d2cf995e27 | ||
|
|
86161aa427 | ||
|
|
a602b4b305 | ||
|
|
047d515abf |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
run_docs: ${{ steps.filter.outputs.run_docs }}
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
@@ -69,6 +70,12 @@ jobs:
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
|
||||
echo "run_nix=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_nix=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
@@ -746,7 +753,10 @@ jobs:
|
||||
nix-build:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries' &&
|
||||
(contains(github.event.pull_request.labels.*.name, 'run-nix') ||
|
||||
needs.job_spec.outputs.run_nix == 'true')
|
||||
secrets: inherit
|
||||
with:
|
||||
flake-output: debug
|
||||
|
||||
46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -1911,7 +1911,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -4133,7 +4132,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4148,6 +4147,8 @@ dependencies = [
|
||||
"async-trait",
|
||||
"collections",
|
||||
"dap",
|
||||
"dotenvy",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"json_dotpath",
|
||||
@@ -4314,14 +4315,11 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -4679,12 +4677,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -4817,6 +4809,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -5117,7 +5110,7 @@ dependencies = [
|
||||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"fs",
|
||||
@@ -8850,6 +8843,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"imara-diff",
|
||||
"indoc",
|
||||
"inventory",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -8948,8 +8942,10 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws_http_client",
|
||||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
@@ -14056,12 +14052,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14069,9 +14066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -14570,16 +14567,28 @@ dependencies = [
|
||||
name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -16013,6 +16022,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"log",
|
||||
"palette",
|
||||
"parking_lot",
|
||||
@@ -20130,9 +20140,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
|
||||
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -444,12 +444,12 @@ core-video = { version = "0.4.3", features = ["metal"] }
|
||||
cpal = "0.16"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
@@ -540,7 +540,7 @@ rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -625,7 +625,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.4"
|
||||
zed_llm_client = "= 0.8.5"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"ctrl-shift-f5": "debugger::RerunSession",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"ctrl-f11": "debugger::StepInto",
|
||||
@@ -598,7 +598,7 @@
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
// or by tag:
|
||||
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
|
||||
"f5": "debugger::RerunLastSession"
|
||||
"f5": "debugger::Rerun"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1067,5 +1067,12 @@
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"bindings": {
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"shift-cmd-f5": "debugger::RerunSession",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"f11": "debugger::StepInto",
|
||||
@@ -652,7 +652,7 @@
|
||||
"cmd-k shift-up": "workspace::SwapPaneUp",
|
||||
"cmd-k shift-down": "workspace::SwapPaneDown",
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"f5": "debugger::RerunLastSession"
|
||||
"f5": "debugger::Rerun"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1167,5 +1167,12 @@
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile"
|
||||
"ctrl-^": "pane::AlternateFile",
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -219,7 +220,6 @@
|
||||
"ctrl-[": "editor::Cancel",
|
||||
"escape": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
".": "vim::Repeat",
|
||||
"c": "vim::PushChange",
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": "vim::PushDelete",
|
||||
@@ -849,6 +849,25 @@
|
||||
"shift-u": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height && VimControl",
|
||||
"bindings": {
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && edit_prediction",
|
||||
"bindings": {
|
||||
@@ -860,14 +879,7 @@
|
||||
{
|
||||
"context": "MessageEditor > Editor && VimControl",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
"enter": "agent::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -96,16 +96,11 @@ impl AgentProfile {
|
||||
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
|
||||
match source {
|
||||
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
|
||||
ToolSource::ContextServer { id } => {
|
||||
if settings.enable_all_context_servers {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
|
||||
return false;
|
||||
};
|
||||
*preset.tools.get(name.as_str()).unwrap_or(&false)
|
||||
}
|
||||
ToolSource::ContextServer { id } => settings
|
||||
.context_servers
|
||||
.get(id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(name.as_str()).copied())
|
||||
.unwrap_or(settings.enable_all_context_servers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,10 @@ use gpui::{
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
|
||||
TokenUsage,
|
||||
LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
|
||||
LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
|
||||
Role, SelectedModel, StopReason, TokenUsage,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::{
|
||||
@@ -1343,6 +1342,7 @@ impl Thread {
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => {
|
||||
let text = text.trim_end();
|
||||
if !text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
@@ -1530,82 +1530,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
let event = match event {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
match error {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
anyhow::bail!(LanguageModelKnownError::RateLimitExceeded { retry_after });
|
||||
}
|
||||
LanguageModelCompletionError::Overloaded => {
|
||||
anyhow::bail!(LanguageModelKnownError::Overloaded);
|
||||
}
|
||||
LanguageModelCompletionError::ApiInternalServerError =>{
|
||||
anyhow::bail!(LanguageModelKnownError::ApiInternalServerError);
|
||||
}
|
||||
LanguageModelCompletionError::PromptTooLarge { tokens } => {
|
||||
let tokens = tokens.unwrap_or_else(|| {
|
||||
// We didn't get an exact token count from the API, so fall back on our estimate.
|
||||
thread.total_token_usage()
|
||||
.map(|usage| usage.total)
|
||||
.unwrap_or(0)
|
||||
// We know the context window was exceeded in practice, so if our estimate was
|
||||
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
|
||||
.max(model.max_token_count().saturating_add(1))
|
||||
});
|
||||
|
||||
anyhow::bail!(LanguageModelKnownError::ContextWindowLimitExceeded { tokens })
|
||||
}
|
||||
LanguageModelCompletionError::ApiReadResponseError(io_error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::ReadResponseError(io_error));
|
||||
}
|
||||
LanguageModelCompletionError::UnknownResponseFormat(error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::UnknownResponseFormat(error));
|
||||
}
|
||||
LanguageModelCompletionError::HttpResponseError { status, ref body } => {
|
||||
if let Some(known_error) = LanguageModelKnownError::from_http_response(status, body) {
|
||||
anyhow::bail!(known_error);
|
||||
} else {
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionError::DeserializeResponse(error) => {
|
||||
anyhow::bail!(LanguageModelKnownError::DeserializeResponse(error));
|
||||
}
|
||||
LanguageModelCompletionError::BadInputJson {
|
||||
id,
|
||||
tool_name,
|
||||
raw_input: invalid_input_json,
|
||||
json_parse_error,
|
||||
} => {
|
||||
thread.receive_invalid_tool_json(
|
||||
id,
|
||||
tool_name,
|
||||
invalid_input_json,
|
||||
json_parse_error,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
// These are all errors we can't automatically attempt to recover from (e.g. by retrying)
|
||||
err @ LanguageModelCompletionError::BadRequestFormat |
|
||||
err @ LanguageModelCompletionError::AuthenticationError |
|
||||
err @ LanguageModelCompletionError::PermissionError |
|
||||
err @ LanguageModelCompletionError::ApiEndpointNotFound |
|
||||
err @ LanguageModelCompletionError::SerializeRequest(_) |
|
||||
err @ LanguageModelCompletionError::BuildRequestBody(_) |
|
||||
err @ LanguageModelCompletionError::HttpSend(_) => {
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
LanguageModelCompletionError::Other(error) => {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
match event? {
|
||||
LanguageModelCompletionEvent::StartMessage { .. } => {
|
||||
request_assistant_message_id =
|
||||
Some(thread.insert_assistant_message(
|
||||
@@ -1682,9 +1607,7 @@ impl Thread {
|
||||
};
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::RedactedThinking {
|
||||
data
|
||||
} => {
|
||||
LanguageModelCompletionEvent::RedactedThinking { data } => {
|
||||
thread.received_chunk();
|
||||
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
@@ -1733,6 +1656,21 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id,
|
||||
tool_name,
|
||||
raw_input: invalid_input_json,
|
||||
json_parse_error,
|
||||
} => {
|
||||
thread.receive_invalid_tool_json(
|
||||
id,
|
||||
tool_name,
|
||||
invalid_input_json,
|
||||
json_parse_error,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
|
||||
if let Some(completion) = thread
|
||||
.pending_completions
|
||||
@@ -1740,23 +1678,34 @@ impl Thread {
|
||||
.find(|completion| completion.id == pending_completion_id)
|
||||
{
|
||||
match status_update {
|
||||
CompletionRequestStatus::Queued {
|
||||
position,
|
||||
} => {
|
||||
completion.queue_state = QueueState::Queued { position };
|
||||
CompletionRequestStatus::Queued { position } => {
|
||||
completion.queue_state =
|
||||
QueueState::Queued { position };
|
||||
}
|
||||
CompletionRequestStatus::Started => {
|
||||
completion.queue_state = QueueState::Started;
|
||||
completion.queue_state = QueueState::Started;
|
||||
}
|
||||
CompletionRequestStatus::Failed {
|
||||
code, message, request_id
|
||||
code,
|
||||
message,
|
||||
request_id: _,
|
||||
retry_after,
|
||||
} => {
|
||||
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
|
||||
return Err(
|
||||
LanguageModelCompletionError::from_cloud_failure(
|
||||
model.upstream_provider_name(),
|
||||
code,
|
||||
message,
|
||||
retry_after.map(Duration::from_secs_f64),
|
||||
),
|
||||
);
|
||||
}
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount, limit
|
||||
} => {
|
||||
thread.update_model_request_usage(amount as u32, limit, cx);
|
||||
CompletionRequestStatus::UsageUpdated { amount, limit } => {
|
||||
thread.update_model_request_usage(
|
||||
amount as u32,
|
||||
limit,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
CompletionRequestStatus::ToolUseLimitReached => {
|
||||
thread.tool_use_limit_reached = true;
|
||||
@@ -1807,10 +1756,11 @@ impl Thread {
|
||||
Ok(stop_reason) => {
|
||||
match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
let tool_uses = thread.use_pending_tools(window, model.clone(), cx);
|
||||
let tool_uses =
|
||||
thread.use_pending_tools(window, model.clone(), cx);
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn | StopReason::MaxTokens => {
|
||||
StopReason::EndTurn | StopReason::MaxTokens => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
@@ -1826,7 +1776,9 @@ impl Thread {
|
||||
{
|
||||
let mut messages_to_remove = Vec::new();
|
||||
|
||||
for (ix, message) in thread.messages.iter().enumerate().rev() {
|
||||
for (ix, message) in
|
||||
thread.messages.iter().enumerate().rev()
|
||||
{
|
||||
messages_to_remove.push(message.id);
|
||||
|
||||
if message.role == Role::User {
|
||||
@@ -1834,7 +1786,9 @@ impl Thread {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_message) = thread.messages.get(ix - 1) {
|
||||
if let Some(prev_message) =
|
||||
thread.messages.get(ix - 1)
|
||||
{
|
||||
if prev_message.role == Role::Assistant {
|
||||
break;
|
||||
}
|
||||
@@ -1849,14 +1803,16 @@ impl Thread {
|
||||
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Language model refusal".into(),
|
||||
message: "Model refused to generate content for safety reasons.".into(),
|
||||
message:
|
||||
"Model refused to generate content for safety reasons."
|
||||
.into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// We successfully completed, so cancel any remaining retries.
|
||||
thread.retry_state = None;
|
||||
},
|
||||
}
|
||||
Err(error) => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
@@ -1882,26 +1838,38 @@ impl Thread {
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::ModelRequestLimitReached { plan: error.plan },
|
||||
));
|
||||
} else if let Some(known_error) =
|
||||
error.downcast_ref::<LanguageModelKnownError>()
|
||||
} else if let Some(completion_error) =
|
||||
error.downcast_ref::<LanguageModelCompletionError>()
|
||||
{
|
||||
match known_error {
|
||||
LanguageModelKnownError::ContextWindowLimitExceeded { tokens } => {
|
||||
use LanguageModelCompletionError::*;
|
||||
match &completion_error {
|
||||
PromptTooLarge { tokens, .. } => {
|
||||
let tokens = tokens.unwrap_or_else(|| {
|
||||
// We didn't get an exact token count from the API, so fall back on our estimate.
|
||||
thread
|
||||
.total_token_usage()
|
||||
.map(|usage| usage.total)
|
||||
.unwrap_or(0)
|
||||
// We know the context window was exceeded in practice, so if our estimate was
|
||||
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
|
||||
.max(model.max_token_count().saturating_add(1))
|
||||
});
|
||||
thread.exceeded_window_error = Some(ExceededWindowError {
|
||||
model_id: model.id(),
|
||||
token_count: *tokens,
|
||||
token_count: tokens,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
LanguageModelKnownError::RateLimitExceeded { retry_after } => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API rate limit exceeded",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
RateLimitExceeded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
}
|
||||
| ServerOverloaded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
} => {
|
||||
thread.handle_rate_limit_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
*retry_after,
|
||||
model.clone(),
|
||||
intent,
|
||||
@@ -1910,15 +1878,9 @@ impl Thread {
|
||||
);
|
||||
retry_scheduled = true;
|
||||
}
|
||||
LanguageModelKnownError::Overloaded => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API servers are overloaded right now",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
RateLimitExceeded { .. } | ServerOverloaded { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
@@ -1928,15 +1890,11 @@ impl Thread {
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
LanguageModelKnownError::ApiInternalServerError => {
|
||||
let provider_name = model.provider_name();
|
||||
let error_message = format!(
|
||||
"{}'s API server reported an internal server error",
|
||||
provider_name.0.as_ref()
|
||||
);
|
||||
|
||||
ApiInternalServerError { .. }
|
||||
| ApiReadResponseError { .. }
|
||||
| HttpSend { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&error_message,
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
@@ -1946,12 +1904,16 @@ impl Thread {
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
LanguageModelKnownError::ReadResponseError(_) |
|
||||
LanguageModelKnownError::DeserializeResponse(_) |
|
||||
LanguageModelKnownError::UnknownResponseFormat(_) => {
|
||||
// In the future we will attempt to re-roll response, but only once
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
NoApiKey { .. }
|
||||
| HttpResponseError { .. }
|
||||
| BadRequestFormat { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| Other { .. } => emit_generic_error(error, cx),
|
||||
}
|
||||
} else {
|
||||
emit_generic_error(error, cx);
|
||||
@@ -2083,7 +2045,7 @@ impl Thread {
|
||||
|
||||
fn handle_rate_limit_error(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
retry_after: Duration,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
@@ -2091,9 +2053,10 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// For rate limit errors, we only retry once with the specified duration
|
||||
let retry_message = format!(
|
||||
"{error_message}. Retrying in {} seconds…",
|
||||
retry_after.as_secs()
|
||||
let retry_message = format!("{error}. Retrying in {} seconds…", retry_after.as_secs());
|
||||
log::warn!(
|
||||
"Retrying completion request in {} seconds: {error:?}",
|
||||
retry_after.as_secs(),
|
||||
);
|
||||
|
||||
// Add a UI-only message instead of a regular message
|
||||
@@ -2126,18 +2089,18 @@ impl Thread {
|
||||
|
||||
fn handle_retryable_error(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.handle_retryable_error_with_delay(error_message, None, model, intent, window, cx)
|
||||
self.handle_retryable_error_with_delay(error, None, model, intent, window, cx)
|
||||
}
|
||||
|
||||
fn handle_retryable_error_with_delay(
|
||||
&mut self,
|
||||
error_message: &str,
|
||||
error: &LanguageModelCompletionError,
|
||||
custom_delay: Option<Duration>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
@@ -2167,8 +2130,12 @@ impl Thread {
|
||||
// Add a transient message to inform the user
|
||||
let delay_secs = delay.as_secs();
|
||||
let retry_message = format!(
|
||||
"{}. Retrying (attempt {} of {}) in {} seconds...",
|
||||
error_message, attempt, max_attempts, delay_secs
|
||||
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds..."
|
||||
);
|
||||
log::warn!(
|
||||
"Retrying completion request (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds: {error:?}",
|
||||
);
|
||||
|
||||
// Add a UI-only message instead of a regular message
|
||||
@@ -4138,9 +4105,15 @@ fn main() {{
|
||||
>,
|
||||
> {
|
||||
let error = match self.error_type {
|
||||
TestError::Overloaded => LanguageModelCompletionError::Overloaded,
|
||||
TestError::Overloaded => LanguageModelCompletionError::ServerOverloaded {
|
||||
provider: self.provider_name(),
|
||||
retry_after: None,
|
||||
},
|
||||
TestError::InternalServerError => {
|
||||
LanguageModelCompletionError::ApiInternalServerError
|
||||
LanguageModelCompletionError::ApiInternalServerError {
|
||||
provider: self.provider_name(),
|
||||
message: "I'm a teapot orbiting the sun".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
async move {
|
||||
@@ -4648,9 +4621,13 @@ fn main() {{
|
||||
> {
|
||||
if !*self.failed_once.lock() {
|
||||
*self.failed_once.lock() = true;
|
||||
let provider = self.provider_name();
|
||||
// Return error on first attempt
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::Overloaded)
|
||||
Err(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
})
|
||||
});
|
||||
async move { Ok(stream.boxed()) }.boxed()
|
||||
} else {
|
||||
@@ -4813,9 +4790,13 @@ fn main() {{
|
||||
> {
|
||||
if !*self.failed_once.lock() {
|
||||
*self.failed_once.lock() = true;
|
||||
let provider = self.provider_name();
|
||||
// Return error on first attempt
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::Overloaded)
|
||||
Err(LanguageModelCompletionError::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
})
|
||||
});
|
||||
async move { Ok(stream.boxed()) }.boxed()
|
||||
} else {
|
||||
@@ -4968,10 +4949,12 @@ fn main() {{
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let provider = self.provider_name();
|
||||
async move {
|
||||
let stream = futures::stream::once(async move {
|
||||
Err(LanguageModelCompletionError::RateLimitExceeded {
|
||||
retry_after: Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS),
|
||||
provider,
|
||||
retry_after: Some(Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS)),
|
||||
})
|
||||
});
|
||||
Ok(stream.boxed())
|
||||
|
||||
@@ -6,9 +6,10 @@ use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
@@ -49,7 +50,7 @@ pub struct AgentSettings {
|
||||
pub dock: AgentDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
@@ -211,7 +212,6 @@ impl AgentSettingsContent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -321,29 +321,27 @@ pub struct LanguageModelSelection {
|
||||
pub struct LanguageModelProviderSetting(pub String);
|
||||
|
||||
impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"LanguageModelProviderSetting".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"amazon-bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
"openrouter".into(),
|
||||
"mistral".into(),
|
||||
"vercel".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"anthropic",
|
||||
"amazon-bedrock",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"ollama",
|
||||
"openai",
|
||||
"zed.dev",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"openrouter",
|
||||
"mistral",
|
||||
"vercel"
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,15 +357,6 @@ impl From<&str> for LanguageModelProviderSetting {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: LanguageModelProviderSetting("openai".to_string()),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
@@ -411,7 +400,10 @@ impl Settings for AgentSettings {
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model.clone());
|
||||
settings.default_model = value
|
||||
.default_model
|
||||
.clone()
|
||||
.or(settings.default_model.take());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
|
||||
@@ -47,8 +47,8 @@ use std::time::Duration;
|
||||
use text::ToPoint;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
|
||||
prelude::*,
|
||||
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
@@ -58,6 +58,7 @@ use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
pub struct ActiveThread {
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -1874,9 +1875,6 @@ impl ActiveThread {
|
||||
this.scroll_to_top(cx);
|
||||
}));
|
||||
|
||||
// For all items that should be aligned with the LLM's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
let show_feedback = thread.is_turn_end(ix);
|
||||
let feedback_container = h_flex()
|
||||
.group("feedback_container")
|
||||
@@ -2537,34 +2535,18 @@ impl ActiveThread {
|
||||
ix: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Stateful<Div> {
|
||||
let colors = cx.theme().colors();
|
||||
div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_sm()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(TextSize::Small.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(message_content),
|
||||
),
|
||||
),
|
||||
)
|
||||
let message = div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(message_content);
|
||||
|
||||
div()
|
||||
.id(("message-container", ix))
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Banner::new().severity(ui::Severity::Warning).child(message))
|
||||
}
|
||||
|
||||
fn render_message_thinking_segment(
|
||||
|
||||
@@ -16,7 +16,9 @@ use gpui::{
|
||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
@@ -86,6 +88,14 @@ impl AgentConfiguration {
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut expanded_provider_configurations = HashMap::default();
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.provider(&ZED_CLOUD_PROVIDER_ID)
|
||||
.map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
|
||||
{
|
||||
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
@@ -94,7 +104,7 @@ impl AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
expanded_provider_configurations,
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
|
||||
@@ -180,7 +180,7 @@ impl ConfigurationSource {
|
||||
}
|
||||
|
||||
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
|
||||
let (name, path, args, env) = match existing {
|
||||
let (name, command, args, env) = match existing {
|
||||
Some((id, cmd)) => {
|
||||
let args = serde_json::to_string(&cmd.args).unwrap();
|
||||
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
|
||||
@@ -198,14 +198,12 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
r#"{{
|
||||
/// The name of your MCP server
|
||||
"{name}": {{
|
||||
"command": {{
|
||||
/// The path to the executable
|
||||
"path": "{path}",
|
||||
/// The arguments to pass to the executable
|
||||
"args": {args},
|
||||
/// The environment variables to set for the executable
|
||||
"env": {env}
|
||||
}}
|
||||
/// The command which runs the MCP server
|
||||
"command": "{command}",
|
||||
/// The arguments to pass to the MCP server
|
||||
"args": {args},
|
||||
/// The environment variables to set
|
||||
"env": {env}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
@@ -439,8 +437,7 @@ fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
|
||||
let object = value.as_object().context("Expected object")?;
|
||||
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
|
||||
let (context_server_name, value) = object.into_iter().next().unwrap();
|
||||
let command = value.get("command").context("Expected command")?;
|
||||
let command: ContextServerCommand = serde_json::from_value(command.clone())?;
|
||||
let command: ContextServerCommand = serde_json::from_value(value.clone())?;
|
||||
Ok((ContextServerId(context_server_name.clone().into()), command))
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
|
||||
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
||||
linear_gradient, prelude::*, pulsating_between,
|
||||
};
|
||||
@@ -59,7 +59,7 @@ use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
@@ -2025,9 +2025,7 @@ impl AgentPanel {
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
});
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
if !is_using_zed_provider {
|
||||
return false;
|
||||
@@ -2600,7 +2598,7 @@ impl AgentPanel {
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.child(Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadtEmptyState,
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
@@ -2691,58 +2689,90 @@ impl AgentPanel {
|
||||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_error_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn upgrade_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme().status().error.opacity(0.08)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
const ERROR_MESSAGE: &str =
|
||||
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(ERROR_MESSAGE))
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Free Usage Exceeded")
|
||||
.description(ERROR_MESSAGE)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(ERROR_MESSAGE))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
@@ -2752,67 +2782,28 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let error_message = match plan {
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
Plan::ZedProTrial => {
|
||||
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
||||
}
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
};
|
||||
let call_to_action = match plan {
|
||||
Plan::ZedPro => "Upgrade to usage-based billing",
|
||||
Plan::ZedProTrial => "Upgrade to Zed Pro",
|
||||
Plan::Free => "Upgrade to Zed Pro",
|
||||
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(error_message))
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Model Prompt Limit Reached")
|
||||
.description(error_message)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(error_message))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
@@ -2823,40 +2814,24 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let message_with_header = format!("{}\n{}", header, message);
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(header).weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_32()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(message_with_header))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title(header)
|
||||
.description(message.clone())
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(message_with_header))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_prompt_editor(
|
||||
@@ -3001,15 +2976,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
@@ -3091,18 +3057,9 @@ impl Render for AgentPanel {
|
||||
thread.clone().into_any_element()
|
||||
})
|
||||
.children(self.render_tool_use_limit_reached(window, cx))
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => {
|
||||
self.render_payment_required_error(thread, cx)
|
||||
@@ -3116,6 +3073,7 @@ impl Render for AgentPanel {
|
||||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::TextThread {
|
||||
|
||||
@@ -92,6 +92,7 @@ actions!(
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
@@ -99,6 +100,7 @@ pub struct NewThread {
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
@@ -209,7 +211,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let default = settings.default_model.as_ref().map(to_selected_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
@@ -229,7 +231,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_default_model(default.as_ref(), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
|
||||
@@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
let current_index = self.selected_index;
|
||||
let active_model = (self.get_active_model)(cx);
|
||||
let bg_executor = cx.background_executor();
|
||||
|
||||
let language_model_registry = LanguageModelRegistry::global(cx);
|
||||
@@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries = filtered_models.entries();
|
||||
// Preserve selection focus
|
||||
let new_index = if current_index >= this.delegate.filtered_entries.len() {
|
||||
0
|
||||
} else {
|
||||
current_index
|
||||
};
|
||||
// Finds the currently selected model in the list
|
||||
let new_index =
|
||||
Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
|
||||
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
|
||||
@@ -1250,9 +1250,7 @@ impl MessageEditor {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
|
||||
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
|
||||
|
||||
@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use http_client::http::{self, HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
@@ -356,7 +356,7 @@ pub async fn complete(
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
let status = response.status();
|
||||
let status_code = response.status();
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
@@ -364,12 +364,12 @@ pub async fn complete(
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
if status.is_success() {
|
||||
if status_code.is_success() {
|
||||
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
|
||||
} else {
|
||||
Err(AnthropicError::HttpResponseError {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
status_code,
|
||||
message: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -444,11 +444,7 @@ impl RateLimitInfo {
|
||||
}
|
||||
|
||||
Self {
|
||||
retry_after: headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs),
|
||||
retry_after: parse_retry_after(headers),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
@@ -457,6 +453,17 @@ impl RateLimitInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
|
||||
/// seconds). Note that other services might specify an HTTP date or some other format for this
|
||||
/// header. Returns `None` if the header is not present or cannot be parsed.
|
||||
pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
|
||||
headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
}
|
||||
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
|
||||
Ok(headers
|
||||
.get(key)
|
||||
@@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if response.status().as_u16() == 529 {
|
||||
Err(AnthropicError::ServerOverloaded {
|
||||
retry_after: rate_limits.retry_after,
|
||||
})
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit { retry_after })
|
||||
} else {
|
||||
@@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
|
||||
match serde_json::from_str::<Event>(&body) {
|
||||
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
|
||||
Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
|
||||
Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status: response.status().as_u16(),
|
||||
body: body,
|
||||
Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status_code: response.status(),
|
||||
message: body,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -801,16 +811,19 @@ pub enum AnthropicError {
|
||||
ReadResponse(io::Error),
|
||||
|
||||
/// HTTP error response from the API
|
||||
HttpResponseError { status: u16, body: String },
|
||||
HttpResponseError {
|
||||
status_code: StatusCode,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Rate limit exceeded
|
||||
RateLimit { retry_after: Duration },
|
||||
|
||||
/// Server overloaded
|
||||
ServerOverloaded { retry_after: Option<Duration> },
|
||||
|
||||
/// API returned an error response
|
||||
ApiError(ApiError),
|
||||
|
||||
/// Unexpected response format
|
||||
UnexpectedResponseFormat(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Error)]
|
||||
|
||||
@@ -2140,7 +2140,8 @@ impl AssistantContext {
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(_) |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use std::{
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::path;
|
||||
|
||||
@@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
Ok(err) => match &err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
|
||||
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
|
||||
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
|
||||
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
);
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{RootSchema, Schema, SchemaObject},
|
||||
JsonSchema, Schema,
|
||||
generate::SchemaSettings,
|
||||
transform::{Transform, transform_subschemas},
|
||||
};
|
||||
|
||||
pub fn json_schema_for<T: JsonSchema>(
|
||||
@@ -13,7 +14,7 @@ pub fn json_schema_for<T: JsonSchema>(
|
||||
}
|
||||
|
||||
fn schema_to_json(
|
||||
schema: &RootSchema,
|
||||
schema: &Schema,
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut value = serde_json::to_value(schema)?;
|
||||
@@ -21,58 +22,42 @@ fn schema_to_json(
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
let mut generator = match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
schemars::r#gen::SchemaSettings::default()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
settings
|
||||
.visitors
|
||||
.push(Box::new(TransformToJsonSchemaSubsetVisitor));
|
||||
})
|
||||
.into_generator()
|
||||
}
|
||||
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
|
||||
// TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
|
||||
// `SchemaSettings::openapi3()`.
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
})
|
||||
.with_transform(ToJsonSchemaSubsetTransform)
|
||||
.into_generator(),
|
||||
};
|
||||
generator.root_schema_for::<T>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransformToJsonSchemaSubsetVisitor;
|
||||
struct ToJsonSchemaSubsetTransform;
|
||||
|
||||
impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
|
||||
fn visit_root_schema(&mut self, root: &mut RootSchema) {
|
||||
schemars::visit::visit_root_schema(self, root)
|
||||
}
|
||||
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
schemars::visit::visit_schema(self, schema)
|
||||
}
|
||||
|
||||
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
|
||||
impl Transform for ToJsonSchemaSubsetTransform {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
// Ensure that the type field is not an array, this happens when we use
|
||||
// Option<T>, the type will be [T, "null"].
|
||||
if let Some(instance_type) = schema.instance_type.take() {
|
||||
schema.instance_type = match instance_type {
|
||||
schemars::schema::SingleOrVec::Single(t) => {
|
||||
Some(schemars::schema::SingleOrVec::Single(t))
|
||||
if let Some(type_field) = schema.get_mut("type") {
|
||||
if let Some(types) = type_field.as_array() {
|
||||
if let Some(first_type) = types.first() {
|
||||
*type_field = first_type.clone();
|
||||
}
|
||||
schemars::schema::SingleOrVec::Vec(items) => items
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(schemars::schema::SingleOrVec::from),
|
||||
};
|
||||
}
|
||||
|
||||
// One of is not supported, use anyOf instead.
|
||||
if let Some(subschema) = schema.subschemas.as_mut() {
|
||||
if let Some(one_of) = subschema.one_of.take() {
|
||||
subschema.any_of = Some(one_of);
|
||||
}
|
||||
}
|
||||
|
||||
schemars::visit::visit_schema_object(self, schema)
|
||||
// oneOf is not supported, use anyOf instead
|
||||
if let Some(one_of) = schema.remove("oneOf") {
|
||||
schema.insert("anyOf".to_string(), one_of);
|
||||
}
|
||||
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,4 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, anyhow};
|
||||
use anyhow::{Context, Error, Result, anyhow};
|
||||
use aws_sdk_bedrockruntime as bedrock;
|
||||
pub use aws_sdk_bedrockruntime as bedrock_client;
|
||||
pub use aws_sdk_bedrockruntime::types::{
|
||||
@@ -24,9 +21,10 @@ pub use bedrock::types::{
|
||||
ToolResultContentBlock as BedrockToolResultContentBlock,
|
||||
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
|
||||
};
|
||||
use futures::stream::{self, BoxStream, Stream};
|
||||
use futures::stream::{self, BoxStream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::models::*;
|
||||
@@ -34,70 +32,59 @@ pub use crate::models::*;
|
||||
pub async fn stream_completion(
|
||||
client: bedrock::Client,
|
||||
request: Request,
|
||||
handle: tokio::runtime::Handle,
|
||||
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
|
||||
handle
|
||||
.spawn(async move {
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
response =
|
||||
response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
])),
|
||||
)])));
|
||||
}
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
let thinking_config = HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
]);
|
||||
response = response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(thinking_config),
|
||||
)])));
|
||||
}
|
||||
|
||||
if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
if request
|
||||
.tools
|
||||
.as_ref()
|
||||
.map_or(false, |t| !t.tools.is_empty())
|
||||
{
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
|
||||
let response = response.send().await;
|
||||
let output = response
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send API request to Bedrock");
|
||||
|
||||
match response {
|
||||
Ok(output) => {
|
||||
let stream: Pin<
|
||||
Box<
|
||||
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
|
||||
+ Send,
|
||||
>,
|
||||
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some(({ Ok(output) }, stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
Some((
|
||||
// TODO: Figure out how we can capture Throttling Exceptions
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
))
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => Err(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
let stream = Box::pin(stream::unfold(
|
||||
output?.stream,
|
||||
move |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some((Ok(output), stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("spawning a task")?
|
||||
},
|
||||
));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn aws_document_to_value(document: &Document) -> Value {
|
||||
|
||||
@@ -12,7 +12,6 @@ pub struct CallSettings {
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
|
||||
@@ -1404,6 +1404,9 @@ async fn sync_model_request_usage_with_stripe(
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("Stripe usage sync: Starting");
|
||||
let started_at = Utc::now();
|
||||
|
||||
let staff_users = app.db.get_staff_users().await?;
|
||||
let staff_user_ids = staff_users
|
||||
.iter()
|
||||
@@ -1448,6 +1451,10 @@ async fn sync_model_request_usage_with_stripe(
|
||||
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
|
||||
.await?;
|
||||
|
||||
let usage_meter_count = usage_meters.len();
|
||||
|
||||
log::info!("Stripe usage sync: Syncing {usage_meter_count} usage meters");
|
||||
|
||||
for (usage_meter, usage) in usage_meters {
|
||||
maybe!(async {
|
||||
let Some((billing_customer, billing_subscription)) =
|
||||
@@ -1504,5 +1511,10 @@ async fn sync_model_request_usage_with_stripe(
|
||||
.log_err();
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Stripe usage sync: Synced {usage_meter_count} usage meters in {:?}",
|
||||
Utc::now() - started_at
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4591,14 +4591,13 @@ async fn test_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::External {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: Some(
|
||||
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
),
|
||||
}]
|
||||
.into(),
|
||||
},
|
||||
)));
|
||||
});
|
||||
});
|
||||
@@ -4699,8 +4698,8 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
|
||||
@@ -505,8 +505,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
|
||||
@@ -41,7 +41,7 @@ pub struct CommandPalette {
|
||||
/// Removes subsequent whitespace characters and double colons from the query.
|
||||
///
|
||||
/// This improves the likelihood of a match by either humanized name or keymap-style name.
|
||||
fn normalize_query(input: &str) -> String {
|
||||
pub fn normalize_action_query(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut last_char = None;
|
||||
|
||||
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let mut commands = self.all_commands.clone();
|
||||
let hit_counts = self.hit_counts();
|
||||
let executor = cx.background_executor().clone();
|
||||
let query = normalize_query(query.as_str());
|
||||
let query = normalize_action_query(query.as_str());
|
||||
async move {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
@@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
tx.send((commands, matches)).await.log_err();
|
||||
}
|
||||
@@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
let matching_command = self.matches.get(ix)?;
|
||||
let command = self.commands.get(matching_command.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
matching_command.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
@@ -512,19 +500,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalize_query() {
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(
|
||||
normalize_query("editor::GoToDefinition"),
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor::::GoToDefinition"),
|
||||
normalize_action_query("editor::::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor: :GoToDefinition"),
|
||||
normalize_action_query("editor: :GoToDefinition"),
|
||||
"editor: :GoToDefinition"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -303,7 +303,6 @@ pub enum ComponentScope {
|
||||
Collaboration,
|
||||
#[strum(serialize = "Data Display")]
|
||||
DataDisplay,
|
||||
Debugger,
|
||||
Editor,
|
||||
#[strum(serialize = "Images & Icons")]
|
||||
Images,
|
||||
|
||||
@@ -29,6 +29,7 @@ impl Display for ContextServerId {
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ContextServerCommand {
|
||||
#[serde(rename = "command")]
|
||||
pub path: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
use language::{LanguageName, LanguageToolchainStore};
|
||||
use node_runtime::NodeRuntime;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::fs::File;
|
||||
@@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct DebugAdapterName(pub SharedString);
|
||||
|
||||
impl Deref for DebugAdapterName {
|
||||
|
||||
@@ -25,7 +25,9 @@ anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
dotenvy.workspace = true
|
||||
futures.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
json_dotpath.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
|
||||
async fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
mut configuration: Value,
|
||||
label: &str,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
let mut configuration = task_definition.config.clone();
|
||||
|
||||
let obj = configuration
|
||||
.as_object_mut()
|
||||
.context("CodeLLDB is not a valid json object")?;
|
||||
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
obj.entry("name")
|
||||
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
|
||||
.or_insert(Value::String(String::from(label)));
|
||||
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
@@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
self.path_to_codelldb.set(path.clone()).ok();
|
||||
command = Some(path);
|
||||
};
|
||||
|
||||
let mut json_config = config.config.clone();
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
arguments: user_args.unwrap_or_else(|| {
|
||||
vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
]
|
||||
if let Some(config) = json_config.as_object_mut()
|
||||
&& let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
|
||||
value
|
||||
.as_array()
|
||||
.map_or(false, |array| array.iter().all(Value::is_string))
|
||||
})
|
||||
{
|
||||
let ret = vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": source_languages}).to_string(),
|
||||
];
|
||||
config.remove("sourceLanguages");
|
||||
ret
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}),
|
||||
request_args: self.request_args(delegate, &config).await?,
|
||||
request_args: self
|
||||
.request_args(delegate, json_config, &config.label)
|
||||
.await?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ mod go;
|
||||
mod javascript;
|
||||
mod php;
|
||||
mod python;
|
||||
mod ruby;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -25,7 +24,6 @@ use gpui::{App, BorrowAppContext};
|
||||
use javascript::JsDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use ruby::RubyDebugAdapter;
|
||||
use serde_json::json;
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
|
||||
@@ -35,7 +33,6 @@ pub fn init(cx: &mut App) {
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(RubyDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
|
||||
@@ -7,13 +7,22 @@ use dap::{
|
||||
latest_github_release,
|
||||
},
|
||||
};
|
||||
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use log::warn;
|
||||
use serde_json::{Map, Value};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util;
|
||||
|
||||
use std::{
|
||||
env::consts,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
adapter_path.join("dlv").to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
let cwd = task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
|
||||
let cwd = Some(
|
||||
task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
|
||||
);
|
||||
|
||||
let arguments;
|
||||
let command;
|
||||
let connection;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
let mut envs = HashMap::default();
|
||||
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
handle_envs(
|
||||
configuration,
|
||||
&mut envs,
|
||||
cwd.as_deref(),
|
||||
delegate.fs().clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(connection_options) = &task_definition.tcp_connection {
|
||||
@@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
cwd,
|
||||
envs,
|
||||
connection,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
@@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// delve doesn't do anything with the envFile setting, so we intercept it
|
||||
async fn handle_envs(
|
||||
config: &mut Map<String, Value>,
|
||||
envs: &mut HashMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> Option<()> {
|
||||
let env_files = match config.get("envFile")? {
|
||||
Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
|
||||
Value::String(s) => vec![Some(s.as_str())],
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rebase_path = |path: PathBuf| {
|
||||
if path.is_absolute() {
|
||||
Some(path)
|
||||
} else {
|
||||
cwd.map(|p| p.join(path))
|
||||
}
|
||||
};
|
||||
|
||||
for path in env_files {
|
||||
let Some(path) = path
|
||||
.and_then(|s| PathBuf::from_str(s).ok())
|
||||
.and_then(rebase_path)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(file) = fs.open_sync(&path).await {
|
||||
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
|
||||
} else {
|
||||
warn!("While starting Go debug session: failed to read env file {path:?}");
|
||||
};
|
||||
}
|
||||
|
||||
// remove envFile now that it's been handled
|
||||
config.remove("entry");
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -282,6 +282,10 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"description": "Automatically stop program after launch",
|
||||
"default": false
|
||||
},
|
||||
"attachSimplePort": {
|
||||
"type": "number",
|
||||
"description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
|
||||
},
|
||||
"runtimeExecutable": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
|
||||
@@ -518,7 +522,11 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
}
|
||||
|
||||
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
let label = args.configuration.get("name")?.as_str()?;
|
||||
let label = args
|
||||
.configuration
|
||||
.get("name")?
|
||||
.as_str()
|
||||
.filter(|name| !name.is_empty())?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsStr, sync::Arc};
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RubyDebugAdapter;
|
||||
|
||||
impl RubyDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Ruby";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RubyDebugConfig {
|
||||
script_or_command: Option<String>,
|
||||
script: Option<String>,
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: FxHashMap<String, String>,
|
||||
cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for RubyDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn adapter_language_name(&self) -> Option<LanguageName> {
|
||||
Some(SharedString::new_static("Ruby").into())
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
},
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a Ruby file."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Directory to execute the program in",
|
||||
"default": "${ZED_WORKTREE_ROOT}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||
"default": {}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
match zed_scenario.request {
|
||||
DebugRequest::Launch(launch) => {
|
||||
let config = RubyDebugConfig {
|
||||
script_or_command: Some(launch.program),
|
||||
script: None,
|
||||
command: None,
|
||||
args: launch.args,
|
||||
env: launch.env,
|
||||
cwd: launch.cwd.clone(),
|
||||
};
|
||||
|
||||
let config = serde_json::to_value(config)?;
|
||||
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
build: None,
|
||||
})
|
||||
}
|
||||
DebugRequest::Attach(_) => {
|
||||
anyhow::bail!("Attach requests are unsupported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
_user_args: Option<Vec<String>>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
|
||||
let mut rdbg_path = adapter_path.join("rdbg");
|
||||
if !delegate.fs().is_file(&rdbg_path).await {
|
||||
match delegate.which("rdbg".as_ref()).await {
|
||||
Some(path) => rdbg_path = path,
|
||||
None => {
|
||||
delegate.output_to_console(
|
||||
"rdbg not found on path, trying `gem install debug`".to_string(),
|
||||
);
|
||||
let output = new_smol_command("gem")
|
||||
.arg("install")
|
||||
.arg("--no-document")
|
||||
.arg("--bindir")
|
||||
.arg(adapter_path)
|
||||
.arg("debug")
|
||||
.output()
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Failed to install rdbg:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
|
||||
|
||||
let mut arguments = vec![
|
||||
"--open".to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
|
||||
if let Some(script) = &ruby_config.script {
|
||||
arguments.push(script.clone());
|
||||
} else if let Some(command) = &ruby_config.command {
|
||||
arguments.push("--command".to_string());
|
||||
arguments.push(command.clone());
|
||||
} else if let Some(command_or_script) = &ruby_config.script_or_command {
|
||||
if delegate
|
||||
.which(OsStr::new(&command_or_script))
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
arguments.push("--command".to_string());
|
||||
}
|
||||
arguments.push(command_or_script.clone());
|
||||
} else {
|
||||
bail!("Ruby debug config must have 'script' or 'command' args");
|
||||
}
|
||||
|
||||
arguments.extend(ruby_config.args);
|
||||
|
||||
let mut configuration = definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(rdbg_path.to_string_lossy().to_string()),
|
||||
arguments,
|
||||
connection: Some(dap::adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: Some(
|
||||
ruby_config
|
||||
.cwd
|
||||
.unwrap_or(delegate.worktree_root_path().to_owned()),
|
||||
),
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config).await?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use project::{
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{BTreeMap, HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::maybe;
|
||||
@@ -32,13 +32,6 @@ use workspace::{
|
||||
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - [x] stop sorting by session ID
|
||||
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
|
||||
// - [ ] dump the launch/attach request somewhere (logs?)
|
||||
|
||||
const MAX_SESSIONS: usize = 10;
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -49,14 +42,34 @@ struct DapLogView {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct LogStoreEntryIdentifier<'a> {
|
||||
session_id: SessionId,
|
||||
project: Cow<'a, WeakEntity<Project>>,
|
||||
}
|
||||
impl LogStoreEntryIdentifier<'_> {
|
||||
fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
|
||||
LogStoreEntryIdentifier {
|
||||
session_id: self.session_id,
|
||||
project: Cow::Owned(self.project.as_ref().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
kind: IoKind,
|
||||
command: Option<SharedString>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
||||
pub struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_sessions: VecDeque<DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
rpc_tx: UnboundedSender<LogStoreMessage>,
|
||||
adapter_log_tx: UnboundedSender<LogStoreMessage>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -122,13 +135,12 @@ impl DebugAdapterState {
|
||||
|
||||
impl LogStore {
|
||||
pub fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
|
||||
while let Some(message) = rpc_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
|
||||
this.add_debug_adapter_message(message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -138,13 +150,12 @@ impl LogStore {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let (adapter_log_tx, mut adapter_log_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
|
||||
while let Some(message) = adapter_log_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_log(session_id, io_kind, message, cx);
|
||||
this.add_debug_adapter_log(message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -157,57 +168,76 @@ impl LogStore {
|
||||
rpc_tx,
|
||||
adapter_log_tx,
|
||||
projects: HashMap::new(),
|
||||
debug_sessions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
|
||||
let weak_project = project.downgrade();
|
||||
self.projects.insert(
|
||||
project.downgrade(),
|
||||
ProjectState {
|
||||
_subscriptions: [
|
||||
cx.observe_release(project, move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
cx.observe_release(project, {
|
||||
let weak_project = project.downgrade();
|
||||
move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
}
|
||||
}),
|
||||
cx.subscribe(
|
||||
&project.read(cx).dap_store(),
|
||||
|this, dap_store, event, cx| match event {
|
||||
cx.subscribe(&project.read(cx).dap_store(), {
|
||||
let weak_project = project.downgrade();
|
||||
move |this, dap_store, event, cx| match event {
|
||||
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
|
||||
let session = dap_store.read(cx).session_by_id(session_id);
|
||||
if let Some(session) = session {
|
||||
this.add_debug_session(*session_id, session, cx);
|
||||
this.add_debug_session(
|
||||
LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(weak_project.clone()),
|
||||
session_id: *session_id,
|
||||
},
|
||||
session,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
|
||||
this.get_debug_adapter_state(*session_id)
|
||||
.iter_mut()
|
||||
.for_each(|state| state.is_terminated = true);
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Borrowed(&weak_project),
|
||||
session_id: *session_id,
|
||||
};
|
||||
if let Some(state) = this.get_debug_adapter_state(&id) {
|
||||
state.is_terminated = true;
|
||||
}
|
||||
|
||||
this.clean_sessions(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
}
|
||||
}),
|
||||
],
|
||||
debug_sessions: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|adapter_state| adapter_state.id == id)
|
||||
fn get_debug_adapter_state(
|
||||
&mut self,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut DebugAdapterState> {
|
||||
self.projects
|
||||
.get_mut(&id.project)
|
||||
.and_then(|state| state.debug_sessions.get_mut(&id.session_id))
|
||||
}
|
||||
|
||||
fn add_debug_adapter_message(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
command: Option<SharedString>,
|
||||
message: SharedString,
|
||||
LogStoreMessage {
|
||||
id,
|
||||
kind: io_kind,
|
||||
command,
|
||||
message,
|
||||
}: LogStoreMessage,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -229,7 +259,7 @@ impl LogStore {
|
||||
if rpc_messages.last_message_kind != Some(kind) {
|
||||
Self::get_debug_adapter_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
kind.label().into(),
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
@@ -239,7 +269,7 @@ impl LogStore {
|
||||
|
||||
let entry = Self::get_debug_adapter_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
message,
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
@@ -260,12 +290,15 @@ impl LogStore {
|
||||
|
||||
fn add_debug_adapter_log(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: SharedString,
|
||||
LogStoreMessage {
|
||||
id,
|
||||
kind: io_kind,
|
||||
message,
|
||||
..
|
||||
}: LogStoreMessage,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
|
||||
let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -276,7 +309,7 @@ impl LogStore {
|
||||
|
||||
Self::get_debug_adapter_entry(
|
||||
&mut debug_adapter_state.log_messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
message,
|
||||
LogKind::Adapter,
|
||||
cx,
|
||||
@@ -286,13 +319,17 @@ impl LogStore {
|
||||
|
||||
fn get_debug_adapter_entry(
|
||||
log_lines: &mut VecDeque<SharedString>,
|
||||
id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
message: SharedString,
|
||||
kind: LogKind,
|
||||
cx: &mut Context<Self>,
|
||||
) -> SharedString {
|
||||
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
|
||||
log_lines.pop_front();
|
||||
if let Some(excess) = log_lines
|
||||
.len()
|
||||
.checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
|
||||
&& excess > 0
|
||||
{
|
||||
log_lines.drain(..excess);
|
||||
}
|
||||
|
||||
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
|
||||
@@ -322,118 +359,116 @@ impl LogStore {
|
||||
|
||||
fn add_debug_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self
|
||||
.debug_sessions
|
||||
.iter_mut()
|
||||
.any(|adapter_state| adapter_state.id == session_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
maybe!({
|
||||
let project_entry = self.projects.get_mut(&id.project)?;
|
||||
let std::collections::btree_map::Entry::Vacant(state) =
|
||||
project_entry.debug_sessions.entry(id.session_id)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map(|client| client.has_adapter_logs())
|
||||
.unwrap_or(false),
|
||||
)
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
|
||||
state.insert(DebugAdapterState::new(
|
||||
id.session_id,
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
self.clean_sessions(cx);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let client = session.read(cx).adapter_client()?;
|
||||
let project = id.project.clone();
|
||||
let session_id = id.session_id;
|
||||
client.add_log_handler(
|
||||
move |kind, command, message| {
|
||||
io_tx
|
||||
.unbounded_send(LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: project.clone(),
|
||||
},
|
||||
kind,
|
||||
command: command.map(|command| command.to_owned().into()),
|
||||
message: message.to_owned().into(),
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
let project = id.project;
|
||||
client.add_log_handler(
|
||||
move |kind, command, message| {
|
||||
log_io_tx
|
||||
.unbounded_send(LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: project.clone(),
|
||||
},
|
||||
kind,
|
||||
command: command.map(|command| command.to_owned().into()),
|
||||
message: message.to_owned().into(),
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
Some(())
|
||||
});
|
||||
|
||||
self.debug_sessions.push_back(DebugAdapterState::new(
|
||||
session_id,
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
self.clean_sessions(cx);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let Some(client) = session.read(cx).adapter_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
log_io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
}
|
||||
|
||||
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
|
||||
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
|
||||
self.debug_sessions.retain(|session| {
|
||||
if to_remove > 0 && session.is_terminated {
|
||||
to_remove -= 1;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
self.projects.values_mut().for_each(|project| {
|
||||
let mut allowed_terminated_sessions = 10u32;
|
||||
project.debug_sessions.retain(|_, session| {
|
||||
if !session.is_terminated {
|
||||
return true;
|
||||
}
|
||||
allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
|
||||
allowed_terminated_sessions > 0
|
||||
});
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn log_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|session| session.id == session_id)
|
||||
self.get_debug_adapter_state(id)
|
||||
.map(|state| &mut state.log_messages)
|
||||
}
|
||||
|
||||
fn rpc_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.messages)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.get_debug_adapter_state(id)
|
||||
.map(|state| &mut state.rpc_messages.messages)
|
||||
}
|
||||
|
||||
fn initialization_sequence_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
) -> Option<&mut Vec<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.initialization_sequence)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&Vec<SharedString>> {
|
||||
self.get_debug_adapter_state(&id)
|
||||
.map(|state| &state.rpc_messages.initialization_sequence)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +488,11 @@ impl Render for DapLogToolbarItemView {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
|
||||
let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
|
||||
(
|
||||
log_view.menu_items(cx),
|
||||
log_view.current_view.map(|(session_id, _)| session_id),
|
||||
log_view.project.downgrade(),
|
||||
)
|
||||
});
|
||||
|
||||
@@ -484,6 +520,7 @@ impl Render for DapLogToolbarItemView {
|
||||
.menu(move |mut window, cx| {
|
||||
let log_view = log_view.clone();
|
||||
let menu_rows = menu_rows.clone();
|
||||
let project = project.clone();
|
||||
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
|
||||
for row in menu_rows.into_iter() {
|
||||
menu = menu.custom_row(move |_window, _cx| {
|
||||
@@ -509,8 +546,15 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(ADAPTER_LOGS))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(row.session_id, window, cx);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(&id, window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -524,8 +568,15 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(RPC_MESSAGES))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.session_id, window, cx);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(&id, window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.custom_entry(
|
||||
@@ -536,12 +587,17 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(INITIALIZATION_SEQUENCE))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_initialization_sequence_for_server(
|
||||
row.session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_initialization_sequence_for_server(
|
||||
&id, window, cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -613,7 +669,9 @@ impl DapLogView {
|
||||
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
||||
Event::NewLogEntry { id, entry, kind } => {
|
||||
if log_view.current_view == Some((*id, *kind)) {
|
||||
if log_view.current_view == Some((id.session_id, *kind))
|
||||
&& log_view.project == *id.project
|
||||
{
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
let last_point = editor.buffer().read(cx).len(cx);
|
||||
@@ -629,12 +687,18 @@ impl DapLogView {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let weak_project = project.downgrade();
|
||||
let state_info = log_store
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.back()
|
||||
.map(|session| (session.id, session.has_adapter_logs));
|
||||
.projects
|
||||
.get(&weak_project)
|
||||
.and_then(|project| {
|
||||
project
|
||||
.debug_sessions
|
||||
.values()
|
||||
.next_back()
|
||||
.map(|session| (session.id, session.has_adapter_logs))
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
editor,
|
||||
@@ -647,10 +711,14 @@ impl DapLogView {
|
||||
};
|
||||
|
||||
if let Some((session_id, have_adapter_logs)) = state_info {
|
||||
let id = LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: Cow::Owned(weak_project),
|
||||
};
|
||||
if have_adapter_logs {
|
||||
this.show_log_messages_for_adapter(session_id, window, cx);
|
||||
this.show_log_messages_for_adapter(&id, window, cx);
|
||||
} else {
|
||||
this.show_rpc_trace_for_server(session_id, window, cx);
|
||||
this.show_rpc_trace_for_server(&id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,31 +758,38 @@ impl DapLogView {
|
||||
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
|
||||
self.log_store
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
.projects
|
||||
.get(&self.project.downgrade())
|
||||
.map_or_else(Vec::new, |state| {
|
||||
state
|
||||
.debug_sessions
|
||||
.values()
|
||||
.rev()
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self
|
||||
.current_view
|
||||
.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.rpc_messages_for_session(session_id)
|
||||
.rpc_messages_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -725,8 +800,7 @@ impl DapLogView {
|
||||
.expect("log buffer should be a singleton")
|
||||
.update(cx, |_, cx| {
|
||||
cx.spawn({
|
||||
let buffer = cx.entity();
|
||||
async move |_, cx| {
|
||||
async move |buffer, cx| {
|
||||
let language = language.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
@@ -746,17 +820,17 @@ impl DapLogView {
|
||||
|
||||
fn show_log_messages_for_adapter(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let message_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.log_messages_for_session(session_id)
|
||||
.log_messages_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((session_id, LogKind::Adapter));
|
||||
self.current_view = Some((id.session_id, LogKind::Adapter));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
@@ -775,17 +849,17 @@ impl DapLogView {
|
||||
|
||||
fn show_initialization_sequence_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.initialization_sequence_for_session(session_id)
|
||||
.initialization_sequence_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -993,9 +1067,9 @@ impl Focusable for DapLogView {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
enum Event {
|
||||
NewLogEntry {
|
||||
id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
entry: SharedString,
|
||||
kind: LogKind,
|
||||
},
|
||||
@@ -1008,31 +1082,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LogStore {
|
||||
pub fn contained_session_ids(&self) -> Vec<SessionId> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.map(|session| session.id)
|
||||
.collect()
|
||||
pub fn has_projects(&self) -> bool {
|
||||
!self.projects.is_empty()
|
||||
}
|
||||
|
||||
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
.clone()
|
||||
.into()
|
||||
pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
|
||||
self.projects.get(project).map_or(vec![], |state| {
|
||||
state.debug_sessions.keys().copied().collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.log_messages
|
||||
.clone()
|
||||
.into()
|
||||
pub fn rpc_messages_for_session_id(
|
||||
&self,
|
||||
project: &WeakEntity<Project>,
|
||||
session_id: SessionId,
|
||||
) -> Vec<SharedString> {
|
||||
self.projects.get(&project).map_or(vec![], |state| {
|
||||
state
|
||||
.debug_sessions
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
.clone()
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,10 @@ bitflags.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
editor.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -68,10 +64,11 @@ theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
ui.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
unindent = { workspace = true, optional = true }
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::session::running::breakpoint_list::BreakpointList;
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
|
||||
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
|
||||
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
|
||||
NewProcessModal, NewProcessMode, Pause, RerunSession, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -25,7 +25,7 @@ use gpui::{
|
||||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Fs, ProjectPath, WorktreeId};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
@@ -197,6 +197,7 @@ impl DebugPanel {
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map(|f| f.worktree_id(cx))
|
||||
});
|
||||
|
||||
let Some(worktree) = worktree
|
||||
.and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
|
||||
.or_else(|| self.project.read(cx).visible_worktrees(cx).next())
|
||||
@@ -204,6 +205,7 @@ impl DebugPanel {
|
||||
log::debug!("Could not find a worktree to spawn the debug session in");
|
||||
return;
|
||||
};
|
||||
|
||||
self.debug_scenario_scheduled_last = true;
|
||||
if let Some(inventory) = self
|
||||
.project
|
||||
@@ -214,7 +216,15 @@ impl DebugPanel {
|
||||
.cloned()
|
||||
{
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory.scenario_scheduled(scenario.clone());
|
||||
inventory.scenario_scheduled(
|
||||
scenario.clone(),
|
||||
// todo(debugger): Task context is cloned three times
|
||||
// once in Session,inventory, and in resolve scenario
|
||||
// we should wrap it in an RC instead to save some memory
|
||||
task_context.clone(),
|
||||
worktree_id,
|
||||
active_buffer.as_ref().map(|buffer| buffer.downgrade()),
|
||||
);
|
||||
})
|
||||
}
|
||||
let task = cx.spawn_in(window, {
|
||||
@@ -225,6 +235,16 @@ impl DebugPanel {
|
||||
let definition = debug_session
|
||||
.update_in(cx, |debug_session, window, cx| {
|
||||
debug_session.running_state().update(cx, |running, cx| {
|
||||
if scenario.build.is_some() {
|
||||
running.scenario = Some(scenario.clone());
|
||||
running.scenario_context = Some(DebugScenarioContext {
|
||||
active_buffer: active_buffer
|
||||
.as_ref()
|
||||
.map(|entity| entity.downgrade()),
|
||||
task_context: task_context.clone(),
|
||||
worktree_id: worktree_id,
|
||||
});
|
||||
};
|
||||
running.resolve_scenario(
|
||||
scenario,
|
||||
task_context,
|
||||
@@ -273,7 +293,8 @@ impl DebugPanel {
|
||||
return;
|
||||
};
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
|
||||
let Some((scenario, context)) = task_inventory.read(cx).last_scheduled_scenario().cloned()
|
||||
else {
|
||||
window.defer(cx, move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -284,28 +305,22 @@ impl DebugPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_contexts = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
worktree_id,
|
||||
active_buffer,
|
||||
} = context;
|
||||
|
||||
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
|
||||
let worktree_id = task_contexts.worktree();
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.start_session(
|
||||
scenario.clone(),
|
||||
task_context,
|
||||
None,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
self.start_session(
|
||||
scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn register_session(
|
||||
@@ -758,16 +773,16 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.restart_session(cx);
|
||||
|this, _, window, cx| {
|
||||
this.rerun_session(window, cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Restart",
|
||||
&Restart,
|
||||
"Rerun Session",
|
||||
&RerunSession,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -868,7 +883,7 @@ impl DebugPanel {
|
||||
let threads =
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let session = running_state.session();
|
||||
session.read(cx).is_running().then(|| {
|
||||
session.read(cx).is_started().then(|| {
|
||||
session.update(cx, |session, cx| {
|
||||
session.threads(cx)
|
||||
})
|
||||
@@ -1298,6 +1313,13 @@ impl Render for DebugPanel {
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.when(!self.is_zoomed, |this| {
|
||||
this.when_else(
|
||||
self.position(window, cx) == DockPosition::Bottom,
|
||||
|this| this.max_h(self.size),
|
||||
|this| this.max_w(self.size),
|
||||
)
|
||||
})
|
||||
.size_full()
|
||||
.key_context("DebugPanel")
|
||||
.child(h_flex().children(self.top_controls_strip(window, cx)))
|
||||
@@ -1468,6 +1490,94 @@ impl Render for DebugPanel {
|
||||
if has_sessions {
|
||||
this.children(self.active_session.clone())
|
||||
} else {
|
||||
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
|
||||
let welcome_experience = v_flex()
|
||||
.when_else(
|
||||
docked_to_bottom,
|
||||
|this| this.w_2_3().h_full().pr_8(),
|
||||
|this| this.w_full().h_1_3(),
|
||||
)
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(crate::Start.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("edit-debug-settings", "Edit debug.json")
|
||||
.icon(IconName::Code)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::OpenProjectDebugTasks.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-debugger-docs", "Debugger Docs")
|
||||
.icon(IconName::Book)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
"spawn-new-session-install-extensions",
|
||||
"Debugger Extensions",
|
||||
)
|
||||
.icon(IconName::Blocks)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::DebugAdapters,
|
||||
),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
let breakpoint_list =
|
||||
v_flex()
|
||||
.group("base-breakpoint-list")
|
||||
.items_start()
|
||||
.when_else(
|
||||
docked_to_bottom,
|
||||
|this| this.min_w_1_3().h_full(),
|
||||
|this| this.w_full().h_2_3(),
|
||||
)
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new("Breakpoints").size(LabelSize::Small))
|
||||
.child(h_flex().visible_on_hover("base-breakpoint-list").child(
|
||||
self.breakpoint_list.read(cx).render_control_strip(),
|
||||
))
|
||||
.track_focus(&self.breakpoint_list.focus_handle(cx)),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
.child(self.breakpoint_list.clone());
|
||||
this.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
@@ -1475,65 +1585,23 @@ impl Render for DebugPanel {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex().size_full()
|
||||
.items_start()
|
||||
|
||||
.child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
|
||||
.child(h_flex().pl_1().w_full().justify_between()
|
||||
.child(Label::new("Breakpoints").size(LabelSize::Small))
|
||||
.child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
|
||||
.child(Divider::horizontal())
|
||||
.child(self.breakpoint_list.clone()))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
v_flex().w_2_3().h_full().items_center().justify_center()
|
||||
.gap_2()
|
||||
.pr_8()
|
||||
.child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(crate::Start.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("edit-debug-settings", "Edit debug.json")
|
||||
.icon(IconName::Code)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("open-debugger-docs", "Debugger Docs")
|
||||
.icon(IconName::Book)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url("https://zed.dev/docs/debugger")
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
|
||||
.icon(IconName::Blocks)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
div()
|
||||
.when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
|
||||
.size_full()
|
||||
.map(|this| {
|
||||
if docked_to_bottom {
|
||||
this.items_start()
|
||||
.child(breakpoint_list)
|
||||
.child(Divider::vertical())
|
||||
.child(welcome_experience)
|
||||
} else {
|
||||
this.items_end()
|
||||
.child(welcome_experience)
|
||||
.child(Divider::horizontal())
|
||||
.child(breakpoint_list)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1549,12 +1617,13 @@ impl workspace::DebuggerProvider for DebuggerProvider {
|
||||
definition: DebugScenario,
|
||||
context: TaskContext,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.0.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.start_session(definition, context, buffer, None, window, cx);
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
this.start_session(definition, context, buffer, worktree_id, window, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ actions!(
|
||||
Detach,
|
||||
Pause,
|
||||
Restart,
|
||||
RerunSession,
|
||||
StepInto,
|
||||
StepOver,
|
||||
StepOut,
|
||||
@@ -54,7 +55,8 @@ actions!(
|
||||
ShowStackTrace,
|
||||
ToggleThreadPicker,
|
||||
ToggleSessionPicker,
|
||||
RerunLastSession,
|
||||
#[action(deprecated_aliases = ["debugger::RerunLastSession"])]
|
||||
Rerun,
|
||||
ToggleExpandItem,
|
||||
]
|
||||
);
|
||||
@@ -74,17 +76,15 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
.register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
@@ -210,6 +210,14 @@ pub fn init(cx: &mut App) {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &RerunSession, window, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.rerun_session(window, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
|
||||
@@ -23,7 +23,9 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use project::{
|
||||
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
|
||||
};
|
||||
use settings::{Settings, initial_local_debug_tasks_content};
|
||||
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
@@ -92,6 +94,7 @@ impl NewProcessModal {
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
|
||||
// todo(debugger): get the buffer here (if the active item is an editor) and store it so we can pass it to start_session later
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?;
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
@@ -1110,7 +1113,11 @@ pub(super) struct TaskMode {
|
||||
|
||||
pub(super) struct DebugDelegate {
|
||||
task_store: Entity<TaskStore>,
|
||||
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
|
||||
candidates: Vec<(
|
||||
Option<TaskSourceKind>,
|
||||
DebugScenario,
|
||||
Option<DebugScenarioContext>,
|
||||
)>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
prompt: String,
|
||||
@@ -1208,7 +1215,11 @@ impl DebugDelegate {
|
||||
|
||||
this.delegate.candidates = recent
|
||||
.into_iter()
|
||||
.map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
|
||||
.map(|(scenario, context)| {
|
||||
let (kind, scenario) =
|
||||
Self::get_scenario_kind(&languages, &dap_registry, scenario);
|
||||
(kind, scenario, Some(context))
|
||||
})
|
||||
.chain(
|
||||
scenarios
|
||||
.into_iter()
|
||||
@@ -1223,7 +1234,7 @@ impl DebugDelegate {
|
||||
.map(|(kind, scenario)| {
|
||||
let (language, scenario) =
|
||||
Self::get_scenario_kind(&languages, &dap_registry, scenario);
|
||||
(language.or(Some(kind)), scenario)
|
||||
(language.or(Some(kind)), scenario, None)
|
||||
}),
|
||||
)
|
||||
.collect();
|
||||
@@ -1269,7 +1280,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
let candidates: Vec<_> = candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (_, candidate))| {
|
||||
.map(|(index, (_, candidate, _))| {
|
||||
StringMatchCandidate::new(index, candidate.label.as_ref())
|
||||
})
|
||||
.collect();
|
||||
@@ -1434,25 +1445,40 @@ impl PickerDelegate for DebugDelegate {
|
||||
.get(self.selected_index())
|
||||
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
|
||||
|
||||
let Some((_, debug_scenario)) = debug_scenario else {
|
||||
let Some((_, debug_scenario, context)) = debug_scenario else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some((
|
||||
task_contexts.active_context().cloned()?,
|
||||
task_contexts.worktree(),
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let context = context.unwrap_or_else(|| {
|
||||
self.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some(DebugScenarioContext {
|
||||
task_context: task_contexts.active_context().cloned()?,
|
||||
active_buffer: None,
|
||||
worktree_id: task_contexts.worktree(),
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use language::Buffer;
|
||||
use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
use project::{
|
||||
Project, WorktreeId,
|
||||
DebugScenarioContext, Project, WorktreeId,
|
||||
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
@@ -79,6 +79,8 @@ pub struct RunningState {
|
||||
pane_close_subscriptions: HashMap<EntityId, Subscription>,
|
||||
dock_axis: Axis,
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
pub(crate) scenario: Option<DebugScenario>,
|
||||
pub(crate) scenario_context: Option<DebugScenarioContext>,
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
@@ -831,6 +833,8 @@ impl RunningState {
|
||||
debug_terminal,
|
||||
dock_axis,
|
||||
_schedule_serialize: None,
|
||||
scenario: None,
|
||||
scenario_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,7 +904,7 @@ impl RunningState {
|
||||
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
let mut extra_config = Value::Null;
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task_template, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
@@ -930,6 +934,7 @@ impl RunningState {
|
||||
};
|
||||
|
||||
let locator_name = if let Some(locator_name) = locator_name {
|
||||
extra_config = config.clone();
|
||||
debug_assert!(!config_is_valid);
|
||||
Some(locator_name)
|
||||
} else if !config_is_valid {
|
||||
@@ -945,6 +950,7 @@ impl RunningState {
|
||||
});
|
||||
if let Ok(t) = task {
|
||||
t.await.and_then(|scenario| {
|
||||
extra_config = scenario.config;
|
||||
match scenario.build {
|
||||
Some(BuildTaskDefinition::Template {
|
||||
locator_name, ..
|
||||
@@ -1008,13 +1014,13 @@ impl RunningState {
|
||||
if !exit_status.success() {
|
||||
anyhow::bail!("Build failed");
|
||||
}
|
||||
Some((task.resolved.clone(), locator_name))
|
||||
Some((task.resolved.clone(), locator_name, extra_config))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
} else if let Some((task, locator_name, extra_config)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.with_context(|| {
|
||||
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
|
||||
@@ -1037,8 +1043,10 @@ impl RunningState {
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
|
||||
.await?;
|
||||
.await?;
|
||||
config = scenario.config;
|
||||
util::merge_non_null_json_value_into(extra_config, &mut config);
|
||||
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
} else {
|
||||
let Err(e) = request_type else {
|
||||
@@ -1521,6 +1529,34 @@ impl RunningState {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn rerun_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some((scenario, context)) = self.scenario.take().zip(self.scenario_context.take())
|
||||
&& scenario.build.is_some()
|
||||
{
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.start_debug_session(
|
||||
scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
self.restart_session(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restart_session(&self, cx: &mut Context<Self>) {
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.restart(None, cx);
|
||||
|
||||
@@ -878,19 +878,30 @@ impl LineBreakpoint {
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(format!("{}:{}", self.name, self.line))
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
)
|
||||
.children(self.dir.clone().map(|dir| {
|
||||
Label::new(dir)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
.children(self.dir.as_ref().and_then(|dir| {
|
||||
let path_without_root = Path::new(dir.as_ref())
|
||||
.components()
|
||||
.skip(1)
|
||||
.collect::<PathBuf>();
|
||||
path_without_root.components().next()?;
|
||||
Some(
|
||||
Label::new(path_without_root.to_string_lossy().into_owned())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
.truncate(),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.when_some(self.dir.as_ref(), |this, parent_dir| {
|
||||
this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
|
||||
})
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
@@ -1234,14 +1245,15 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-log-toggle")),
|
||||
IconName::ScrollText,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
|
||||
.icon_color(color_for_toggle(has_logs))
|
||||
.disabled(!supports_logs)
|
||||
@@ -1261,6 +1273,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
SharedString::from(format!("{id}-condition-toggle")),
|
||||
IconName::SplitAlt,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
has_condition
|
||||
@@ -1274,7 +1287,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
.when(!has_condition && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div() .map(self.add_border(
|
||||
div().map(self.add_border(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
supports_hit_condition,window, cx
|
||||
))
|
||||
@@ -1283,6 +1296,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
SharedString::from(format!("{id}-hit-condition-toggle")),
|
||||
IconName::ArrowDown10,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
has_hit_condition,
|
||||
|
||||
@@ -114,7 +114,7 @@ impl Console {
|
||||
}
|
||||
|
||||
fn is_running(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_running()
|
||||
self.session.read(cx).is_started()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
|
||||
@@ -115,6 +115,7 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||
config.to_scenario(),
|
||||
TaskContext::default(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids()
|
||||
.is_empty()),
|
||||
"log_store shouldn't contain any session IDs before any sessions were created"
|
||||
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.is_empty()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Start a debug session
|
||||
@@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.len()),
|
||||
1,
|
||||
);
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids()
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.contains(&session_id)),
|
||||
"log_store should contain the session IDs of the started session"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!log_store.read_with(cx, |log_store, _| log_store
|
||||
.rpc_messages_for_session_id(session_id)
|
||||
.rpc_messages_for_session_id(&project.downgrade(), session_id)
|
||||
.is_empty()),
|
||||
"We should have the initialization sequence in the log store"
|
||||
);
|
||||
|
||||
@@ -141,7 +141,14 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.start_debug_session(scenario, task_context.clone(), None, window, cx)
|
||||
workspace.start_debug_session(
|
||||
scenario,
|
||||
task_context.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -267,7 +274,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
|
||||
"Debugpy",
|
||||
"PHP",
|
||||
"JavaScript",
|
||||
"Ruby",
|
||||
"Delve",
|
||||
"GDB",
|
||||
"fake-adapter",
|
||||
|
||||
@@ -61,6 +61,7 @@ parking_lot.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -37,7 +37,9 @@ pub use block_map::{
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use crease_map::*;
|
||||
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
|
||||
pub use fold_map::{
|
||||
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
|
||||
};
|
||||
use fold_map::{FoldMap, FoldSnapshot};
|
||||
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
|
||||
pub use inlay_map::Inlay;
|
||||
@@ -538,7 +540,7 @@ impl DisplayMap {
|
||||
|
||||
pub fn update_fold_widths(
|
||||
&mut self,
|
||||
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::{InlayId, display_map::inlay_map::InlayChunk};
|
||||
|
||||
use super::{
|
||||
Highlights,
|
||||
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
|
||||
@@ -275,13 +277,16 @@ impl FoldMapWriter<'_> {
|
||||
|
||||
pub(crate) fn update_fold_widths(
|
||||
&mut self,
|
||||
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||
let mut edits = Vec::new();
|
||||
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
|
||||
let buffer = &inlay_snapshot.buffer;
|
||||
|
||||
for (id, new_width) in new_widths {
|
||||
let ChunkRendererId::Fold(id) = id else {
|
||||
continue;
|
||||
};
|
||||
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
|
||||
if Some(new_width) != metadata.width {
|
||||
let buffer_start = metadata.range.start.to_offset(buffer);
|
||||
@@ -527,7 +532,7 @@ impl FoldMap {
|
||||
placeholder: Some(TransformPlaceholder {
|
||||
text: ELLIPSIS,
|
||||
renderer: ChunkRenderer {
|
||||
id: fold.id,
|
||||
id: ChunkRendererId::Fold(fold.id),
|
||||
render: Arc::new(move |cx| {
|
||||
(fold.placeholder.render)(
|
||||
fold_id,
|
||||
@@ -1060,7 +1065,7 @@ impl sum_tree::Summary for TransformSummary {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
|
||||
pub struct FoldId(usize);
|
||||
pub struct FoldId(pub(super) usize);
|
||||
|
||||
impl From<FoldId> for ElementId {
|
||||
fn from(val: FoldId) -> Self {
|
||||
@@ -1265,11 +1270,17 @@ pub struct Chunk<'a> {
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChunkRendererId {
|
||||
Fold(FoldId),
|
||||
Inlay(InlayId),
|
||||
}
|
||||
|
||||
/// A recipe for how the chunk should be presented.
|
||||
#[derive(Clone)]
|
||||
pub struct ChunkRenderer {
|
||||
/// The id of the fold associated with this chunk.
|
||||
pub id: FoldId,
|
||||
/// The id of the renderer associated with this chunk.
|
||||
pub id: ChunkRendererId,
|
||||
/// Creates a custom element to represent this chunk.
|
||||
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
|
||||
/// If true, the element is constrained to the shaped width of the text.
|
||||
@@ -1311,7 +1322,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
|
||||
pub struct FoldChunks<'a> {
|
||||
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
|
||||
inlay_chunks: InlayChunks<'a>,
|
||||
inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
|
||||
inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>,
|
||||
inlay_offset: InlayOffset,
|
||||
output_offset: FoldOffset,
|
||||
max_output_offset: FoldOffset,
|
||||
@@ -1403,7 +1414,8 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
}
|
||||
|
||||
// Otherwise, take a chunk from the buffer's text.
|
||||
if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
|
||||
if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
|
||||
let chunk = &mut inlay_chunk.chunk;
|
||||
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
|
||||
let transform_end = self.transform_cursor.end(&()).1;
|
||||
let chunk_end = buffer_chunk_end.min(transform_end);
|
||||
@@ -1428,7 +1440,7 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
is_tab: chunk.is_tab,
|
||||
is_inlay: chunk.is_inlay,
|
||||
underline: chunk.underline,
|
||||
renderer: None,
|
||||
renderer: inlay_chunk.renderer,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{HighlightStyles, InlayId};
|
||||
use crate::{ChunkRenderer, HighlightStyles, InlayId};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{Hsla, Rgba};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
@@ -8,11 +8,13 @@ use multi_buffer::{
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Add, AddAssign, Range, Sub, SubAssign},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, SumTree};
|
||||
use text::{Patch, Rope};
|
||||
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
|
||||
|
||||
use super::{Highlights, custom_highlights::CustomHighlightsChunks};
|
||||
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
|
||||
|
||||
/// Decides where the [`Inlay`]s should be displayed.
|
||||
///
|
||||
@@ -252,6 +254,13 @@ pub struct InlayChunks<'a> {
|
||||
snapshot: &'a InlaySnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InlayChunk<'a> {
|
||||
pub chunk: Chunk<'a>,
|
||||
/// Whether the inlay should be customly rendered.
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
}
|
||||
|
||||
impl InlayChunks<'_> {
|
||||
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
|
||||
self.transforms.seek(&new_range.start, Bias::Right, &());
|
||||
@@ -271,7 +280,7 @@ impl InlayChunks<'_> {
|
||||
}
|
||||
|
||||
impl<'a> Iterator for InlayChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
type Item = InlayChunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.output_offset == self.max_output_offset {
|
||||
@@ -296,9 +305,12 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
|
||||
chunk.text = suffix;
|
||||
self.output_offset.0 += prefix.len();
|
||||
Chunk {
|
||||
text: prefix,
|
||||
..chunk.clone()
|
||||
InlayChunk {
|
||||
chunk: Chunk {
|
||||
text: prefix,
|
||||
..chunk.clone()
|
||||
},
|
||||
renderer: None,
|
||||
}
|
||||
}
|
||||
Transform::Inlay(inlay) => {
|
||||
@@ -313,6 +325,7 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut renderer = None;
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::InlineCompletion(_) => {
|
||||
self.highlight_styles.inline_completion.map(|s| {
|
||||
@@ -325,14 +338,31 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
}
|
||||
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
|
||||
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
|
||||
InlayId::Color(_) => match inlay.color {
|
||||
Some(color) => {
|
||||
let mut style = self.highlight_styles.inlay_hint.unwrap_or_default();
|
||||
style.color = Some(color);
|
||||
Some(style)
|
||||
InlayId::Color(_) => {
|
||||
if let Some(color) = inlay.color {
|
||||
renderer = Some(ChunkRenderer {
|
||||
id: ChunkRendererId::Inlay(inlay.id),
|
||||
render: Arc::new(move |cx| {
|
||||
div()
|
||||
.relative()
|
||||
.size_3p5()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.size_3()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(color),
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
constrain_width: false,
|
||||
measured_width: None,
|
||||
});
|
||||
}
|
||||
None => self.highlight_styles.inlay_hint,
|
||||
},
|
||||
self.highlight_styles.inlay_hint
|
||||
}
|
||||
};
|
||||
let next_inlay_highlight_endpoint;
|
||||
let offset_in_inlay = self.output_offset - self.transforms.start().0;
|
||||
@@ -370,11 +400,14 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
|
||||
self.output_offset.0 += chunk.len();
|
||||
|
||||
Chunk {
|
||||
text: chunk,
|
||||
highlight_style,
|
||||
is_inlay: true,
|
||||
..Default::default()
|
||||
InlayChunk {
|
||||
chunk: Chunk {
|
||||
text: chunk,
|
||||
highlight_style,
|
||||
is_inlay: true,
|
||||
..Chunk::default()
|
||||
},
|
||||
renderer,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1066,7 +1099,7 @@ impl InlaySnapshot {
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(Default::default()..self.len(), false, Highlights::default())
|
||||
.map(|chunk| chunk.text)
|
||||
.map(|chunk| chunk.chunk.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1704,7 +1737,7 @@ mod tests {
|
||||
..Highlights::default()
|
||||
},
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.map(|chunk| chunk.chunk.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
actual_text,
|
||||
|
||||
@@ -21,7 +21,6 @@ mod editor_settings;
|
||||
mod editor_settings_controls;
|
||||
mod element;
|
||||
mod git;
|
||||
mod gutter;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
pub mod hover_popover;
|
||||
@@ -202,8 +201,8 @@ use theme::{
|
||||
observe_buffer_font_size_adjustment,
|
||||
};
|
||||
use ui::{
|
||||
ButtonSize, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, IconSize,
|
||||
Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
@@ -548,6 +547,7 @@ pub enum SoftWrap {
|
||||
#[derive(Clone)]
|
||||
pub struct EditorStyle {
|
||||
pub background: Hsla,
|
||||
pub border: Hsla,
|
||||
pub local_player: PlayerColor,
|
||||
pub text: TextStyle,
|
||||
pub scrollbar_width: Pixels,
|
||||
@@ -563,6 +563,7 @@ impl Default for EditorStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background: Hsla::default(),
|
||||
border: Hsla::default(),
|
||||
local_player: PlayerColor::default(),
|
||||
text: TextStyle::default(),
|
||||
scrollbar_width: Pixels::default(),
|
||||
@@ -6185,7 +6186,14 @@ impl Editor {
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx);
|
||||
workspace.start_debug_session(scenario, context, Some(buffer), window, cx);
|
||||
workspace.start_debug_session(
|
||||
scenario,
|
||||
context,
|
||||
Some(buffer),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
Some(Task::ready(Ok(())))
|
||||
}
|
||||
@@ -7978,6 +7986,121 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
breakpoint: &Breakpoint,
|
||||
state: Option<BreakpointSessionState>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let is_rejected = state.is_some_and(|s| !s.verified);
|
||||
// Is it a breakpoint that shows up when hovering over gutter?
|
||||
let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or(
|
||||
(false, false),
|
||||
|PhantomBreakpointIndicator {
|
||||
is_active,
|
||||
display_row,
|
||||
collides_with_existing_breakpoint,
|
||||
}| {
|
||||
(
|
||||
is_active && display_row == row,
|
||||
collides_with_existing_breakpoint,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let (color, icon) = {
|
||||
let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) {
|
||||
(false, false) => ui::IconName::DebugBreakpoint,
|
||||
(true, false) => ui::IconName::DebugLogBreakpoint,
|
||||
(false, true) => ui::IconName::DebugDisabledBreakpoint,
|
||||
(true, true) => ui::IconName::DebugDisabledLogBreakpoint,
|
||||
};
|
||||
|
||||
let color = if is_phantom {
|
||||
Color::Hint
|
||||
} else if is_rejected {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
|
||||
(color, icon)
|
||||
};
|
||||
|
||||
let breakpoint = Arc::from(breakpoint.clone());
|
||||
|
||||
let alt_as_text = gpui::Keystroke {
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
..Default::default()
|
||||
};
|
||||
let primary_action_text = if breakpoint.is_disabled() {
|
||||
"Enable breakpoint"
|
||||
} else if is_phantom && !collides_with_existing {
|
||||
"Set breakpoint"
|
||||
} else {
|
||||
"Unset breakpoint"
|
||||
};
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let meta = if is_rejected {
|
||||
SharedString::from("No executable code is associated with this line.")
|
||||
} else if collides_with_existing && !breakpoint.is_disabled() {
|
||||
SharedString::from(format!(
|
||||
"{alt_as_text}-click to disable,\nright-click for more options."
|
||||
))
|
||||
} else {
|
||||
SharedString::from("Right-click for more options.")
|
||||
};
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.when(is_rejected, |this| {
|
||||
this.indicator(Indicator::icon(Icon::new(IconName::Warning)).color(Color::Warning))
|
||||
})
|
||||
.icon_color(color)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener({
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |editor, event: &ClickEvent, window, cx| {
|
||||
let edit_action = if event.modifiers().platform || breakpoint.is_disabled() {
|
||||
BreakpointEditAction::InvertState
|
||||
} else {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(position),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
primary_action_text,
|
||||
Some(&ToggleBreakpoint),
|
||||
meta.clone(),
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -11425,66 +11548,90 @@ impl Editor {
|
||||
let language_settings = buffer.language_settings_at(selection.head(), cx);
|
||||
let language_scope = buffer.language_scope_at(selection.head());
|
||||
|
||||
let indent_and_prefix_for_row =
|
||||
|row: u32| -> (IndentSize, Option<String>, Option<String>) {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let (comment_prefix, rewrap_prefix) =
|
||||
if let Some(language_scope) = &language_scope {
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
let comment_prefix = language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.map(|prefix| prefix.to_string());
|
||||
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
|
||||
let line_text_after_indent = buffer
|
||||
.text_for_range(indent_end..line_end)
|
||||
.collect::<String>();
|
||||
let rewrap_prefix = language_scope
|
||||
.rewrap_prefixes()
|
||||
.iter()
|
||||
.find_map(|prefix_regex| {
|
||||
prefix_regex.find(&line_text_after_indent).map(|mat| {
|
||||
if mat.start() == 0 {
|
||||
Some(mat.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
(comment_prefix, rewrap_prefix)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
(indent, comment_prefix, rewrap_prefix)
|
||||
};
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
let mut current_range_start = first_row;
|
||||
let from_empty_selection = selection.is_empty();
|
||||
|
||||
let mut current_range_start = first_row;
|
||||
let mut prev_row = first_row;
|
||||
let mut prev_indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
|
||||
let mut prev_comment_prefix = if let Some(language_scope) = &language_scope {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
|
||||
let indent_end = Point::new(first_row, indent.len);
|
||||
language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (
|
||||
mut current_range_indent,
|
||||
mut current_range_comment_prefix,
|
||||
mut current_range_rewrap_prefix,
|
||||
) = indent_and_prefix_for_row(first_row);
|
||||
|
||||
for row in non_blank_rows_iter.skip(1) {
|
||||
let has_paragraph_break = row > prev_row + 1;
|
||||
|
||||
let row_indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let row_comment_prefix = if let Some(language_scope) = &language_scope {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (row_indent, row_comment_prefix, row_rewrap_prefix) =
|
||||
indent_and_prefix_for_row(row);
|
||||
|
||||
let has_boundary_change =
|
||||
row_indent != prev_indent || row_comment_prefix != prev_comment_prefix;
|
||||
let has_indent_change = row_indent != current_range_indent;
|
||||
let has_comment_change = row_comment_prefix != current_range_comment_prefix;
|
||||
|
||||
let has_boundary_change = has_comment_change
|
||||
|| row_rewrap_prefix.is_some()
|
||||
|| (has_indent_change && current_range_comment_prefix.is_some());
|
||||
|
||||
if has_paragraph_break || has_boundary_change {
|
||||
ranges.push((
|
||||
language_settings.clone(),
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
prev_indent,
|
||||
prev_comment_prefix.clone(),
|
||||
current_range_indent,
|
||||
current_range_comment_prefix.clone(),
|
||||
current_range_rewrap_prefix.clone(),
|
||||
from_empty_selection,
|
||||
));
|
||||
current_range_start = row;
|
||||
current_range_indent = row_indent;
|
||||
current_range_comment_prefix = row_comment_prefix;
|
||||
current_range_rewrap_prefix = row_rewrap_prefix;
|
||||
}
|
||||
|
||||
prev_row = row;
|
||||
prev_indent = row_indent;
|
||||
prev_comment_prefix = row_comment_prefix;
|
||||
}
|
||||
|
||||
ranges.push((
|
||||
language_settings.clone(),
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
prev_indent,
|
||||
prev_comment_prefix,
|
||||
current_range_indent,
|
||||
current_range_comment_prefix,
|
||||
current_range_rewrap_prefix,
|
||||
from_empty_selection,
|
||||
));
|
||||
|
||||
@@ -11494,8 +11641,14 @@ impl Editor {
|
||||
let mut edits = Vec::new();
|
||||
let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
|
||||
|
||||
for (language_settings, wrap_range, indent_size, comment_prefix, from_empty_selection) in
|
||||
wrap_ranges
|
||||
for (
|
||||
language_settings,
|
||||
wrap_range,
|
||||
indent_size,
|
||||
comment_prefix,
|
||||
rewrap_prefix,
|
||||
from_empty_selection,
|
||||
) in wrap_ranges
|
||||
{
|
||||
let mut start_row = wrap_range.start.row;
|
||||
let mut end_row = wrap_range.end.row;
|
||||
@@ -11511,12 +11664,16 @@ impl Editor {
|
||||
|
||||
let tab_size = language_settings.tab_size;
|
||||
|
||||
let mut line_prefix = indent_size.chars().collect::<String>();
|
||||
let indent_prefix = indent_size.chars().collect::<String>();
|
||||
let mut line_prefix = indent_prefix.clone();
|
||||
let mut inside_comment = false;
|
||||
if let Some(prefix) = &comment_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
inside_comment = true;
|
||||
}
|
||||
if let Some(prefix) = &rewrap_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
}
|
||||
|
||||
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
|
||||
RewrapBehavior::InComments => inside_comment,
|
||||
@@ -11563,12 +11720,18 @@ impl Editor {
|
||||
let selection_text = buffer.text_for_range(start..end).collect::<String>();
|
||||
let Some(lines_without_prefixes) = selection_text
|
||||
.lines()
|
||||
.map(|line| {
|
||||
line.strip_prefix(&line_prefix)
|
||||
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
|
||||
.with_context(|| {
|
||||
format!("line did not start with prefix {line_prefix:?}: {line:?}")
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(ix, line)| {
|
||||
let line_trimmed = line.trim_start();
|
||||
if rewrap_prefix.is_some() && ix > 0 {
|
||||
Ok(line_trimmed)
|
||||
} else {
|
||||
line_trimmed
|
||||
.strip_prefix(&line_prefix.trim_start())
|
||||
.with_context(|| {
|
||||
format!("line did not start with prefix {line_prefix:?}: {line:?}")
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.log_err()
|
||||
@@ -11581,8 +11744,16 @@ impl Editor {
|
||||
.language_settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize
|
||||
});
|
||||
|
||||
let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
|
||||
format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
|
||||
} else {
|
||||
line_prefix.clone()
|
||||
};
|
||||
|
||||
let wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
subsequent_lines_prefix,
|
||||
lines_without_prefixes.join("\n"),
|
||||
wrap_column,
|
||||
tab_size,
|
||||
@@ -17217,9 +17388,9 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
}
|
||||
|
||||
pub fn update_fold_widths(
|
||||
pub fn update_renderer_widths(
|
||||
&mut self,
|
||||
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.display_map
|
||||
@@ -21084,18 +21255,22 @@ fn test_word_breaking_tokenizer() {
|
||||
}
|
||||
|
||||
fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
first_line_prefix: String,
|
||||
subsequent_lines_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size);
|
||||
let subsequent_lines_prefix_len =
|
||||
char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
let mut current_line = first_line_prefix.clone();
|
||||
let mut is_first_line = true;
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut current_line_len = first_line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
@@ -21105,13 +21280,19 @@ fn wrap_with_prefix(
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
&& current_line_len != current_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
@@ -21128,32 +21309,46 @@ fn wrap_with_prefix(
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if current_line_len != current_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
} else if current_line_len + 1 > wrap_column
|
||||
&& current_line_len != current_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if current_line_len != current_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
@@ -21171,6 +21366,7 @@ fn wrap_with_prefix(
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
@@ -21181,6 +21377,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
@@ -21191,6 +21388,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
@@ -21201,6 +21399,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
@@ -22291,6 +22490,7 @@ impl Render for Editor {
|
||||
&cx.entity(),
|
||||
EditorStyle {
|
||||
background,
|
||||
border: cx.theme().colors().border,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
scrollbar_width: EditorElement::SCROLLBAR_WIDTH,
|
||||
|
||||
@@ -378,7 +378,6 @@ pub enum SnippetSortOrder {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct EditorSettingsContent {
|
||||
/// Whether the cursor blinks in the editor.
|
||||
///
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use gpui::{App, FontFeatures, FontWeight};
|
||||
use project::project_settings::{InlineBlameSettings, ProjectSettings};
|
||||
use settings::{EditableSettingControl, Settings};
|
||||
use theme::{FontFamilyCache, ThemeSettings};
|
||||
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
||||
use ui::{
|
||||
CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
|
||||
prelude::*,
|
||||
@@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl {
|
||||
value: Self::Value,
|
||||
_cx: &App,
|
||||
) {
|
||||
settings.buffer_font_family = Some(value.to_string());
|
||||
settings.buffer_font_family = Some(FontFamilyName(value.into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ use language::{
|
||||
DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
|
||||
LanguageName, Override, Point,
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
LanguageSettingsContent, LspInsertMode, PrettierSettings,
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
|
||||
LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
|
||||
},
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use language_settings::{Formatter, IndentGuideSettings};
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{IndentGuide, PathKey};
|
||||
use parking_lot::Mutex;
|
||||
@@ -3567,7 +3567,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"TOML".into(),
|
||||
LanguageSettingsContent {
|
||||
@@ -5145,7 +5145,7 @@ fn test_transpose(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"Markdown".into(),
|
||||
LanguageSettingsContent {
|
||||
@@ -5210,6 +5210,10 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
let markdown_language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Markdown".into(),
|
||||
rewrap_prefixes: vec![
|
||||
regex::Regex::new("\\d+\\.\\s+").unwrap(),
|
||||
regex::Regex::new("[-*+]\\s+").unwrap(),
|
||||
],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
@@ -5372,7 +5376,82 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
A long long long line of markdown text
|
||||
to wrap.ˇ
|
||||
"},
|
||||
markdown_language,
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping boundary works and preserves relative indent for Markdown documents
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is very long and needs to be wrapped properly.
|
||||
2. This is a numbered list item that is very long and needs to be wrapped properly.
|
||||
- This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping add indents for rewrapping boundary if not exists already.
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge with
|
||||
the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping maintain indents even when they already exists.
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list
|
||||
item that is very long and needs to be wrapped properly.
|
||||
2. This is a numbered list
|
||||
item that is very long and needs to be wrapped properly.
|
||||
- This is an unordered list item that is also very long and
|
||||
should not merge with the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
@@ -9326,7 +9405,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
settings.languages.0.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -9890,7 +9969,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
|
||||
|
||||
// Set Rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
settings.languages.0.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -9933,9 +10012,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -10062,21 +10141,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
settings.defaults.formatter =
|
||||
Some(language_settings::SelectedFormatter::List(FormatterList(
|
||||
vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::CodeActions(
|
||||
[
|
||||
("code-action-1".into(), true),
|
||||
("code-action-2".into(), true),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::CodeActions(
|
||||
[
|
||||
("code-action-1".into(), true),
|
||||
("code-action-2".into(), true),
|
||||
]
|
||||
.into(),
|
||||
)))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
])))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -10328,9 +10403,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
])))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -10536,7 +10611,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
||||
settings.defaults.formatter = Some(SelectedFormatter::Auto)
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -14905,7 +14980,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
.unwrap();
|
||||
let _fake_server = fake_servers.next().await.unwrap();
|
||||
update_test_language_settings(cx, |language_settings| {
|
||||
language_settings.languages.insert(
|
||||
language_settings.languages.0.insert(
|
||||
language_name.clone(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -15803,9 +15878,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::Prettier].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::Prettier,
|
||||
)))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -15875,7 +15950,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
||||
settings.defaults.formatter = Some(SelectedFormatter::Auto)
|
||||
});
|
||||
let format = editor.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
ToggleFold,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
|
||||
HighlightedChunk, ToDisplayPoint,
|
||||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||||
HighlightKey, HighlightedChunk, ToDisplayPoint,
|
||||
},
|
||||
editor_settings::{
|
||||
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
|
||||
@@ -21,7 +21,6 @@ use crate::{
|
||||
ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
|
||||
},
|
||||
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||||
gutter::breakpoint_indicator::breakpoint_indicator_path,
|
||||
hover_popover::{
|
||||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
POPOVER_RIGHT_OFFSET, hover_at,
|
||||
@@ -31,7 +30,6 @@ use crate::{
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::{ActiveScrollbarState, ScrollbarThumbState, scroll_amount::ScrollAmount},
|
||||
};
|
||||
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
@@ -44,12 +42,12 @@ use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
|
||||
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, Modifiers,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
|
||||
Window, anchored, canvas, deferred, div, fill, linear_color_stop, linear_gradient, outline,
|
||||
point, px, quad, relative, size, solid_background, transparent_black,
|
||||
Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
|
||||
quad, relative, size, solid_background, transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
@@ -63,7 +61,7 @@ use multi_buffer::{
|
||||
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointEditAction, BreakpointSessionState},
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
@@ -2759,16 +2757,7 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let button = self.render_breakpoint(
|
||||
editor,
|
||||
snapshot,
|
||||
text_anchor,
|
||||
display_row,
|
||||
row,
|
||||
&bp,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
@@ -2787,184 +2776,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
editor: &Editor,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
multibuffer_row: MultiBufferRow,
|
||||
breakpoint: &Breakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let element_id =
|
||||
ElementId::Name(format!("breakpoint_indicator_{}", multibuffer_row.0).into());
|
||||
|
||||
// === Extract text style and calculate dimensions ===
|
||||
let text_style = self.style.text.clone();
|
||||
let font_size = text_style.font_size;
|
||||
let font_size_px = font_size.to_pixels(window.rem_size());
|
||||
let rem_size = window.rem_size();
|
||||
|
||||
// Calculate font scale relative to a baseline font size
|
||||
const BASELINE_FONT_SIZE: f32 = 14.0; // Default editor font size
|
||||
let font_scale = font_size_px / px(BASELINE_FONT_SIZE);
|
||||
|
||||
let line_height: Pixels = text_style.line_height.to_pixels(font_size, rem_size);
|
||||
|
||||
// Debug font metrics
|
||||
dbg!(font_size);
|
||||
dbg!(font_size_px);
|
||||
dbg!(rem_size);
|
||||
dbg!(BASELINE_FONT_SIZE);
|
||||
dbg!(font_scale);
|
||||
dbg!(line_height);
|
||||
|
||||
// Helper to scale pixel values based on font size
|
||||
let scale_px = |value: f32| px(value) * font_scale;
|
||||
|
||||
const HORIZONTAL_OFFSET: f32 = 40.0;
|
||||
const VERTICAL_OFFSET: f32 = 4.0;
|
||||
|
||||
let horizontal_offset = scale_px(HORIZONTAL_OFFSET);
|
||||
let vertical_offset = px(VERTICAL_OFFSET);
|
||||
let indicator_height = line_height - vertical_offset;
|
||||
|
||||
let line_number_width = self.max_line_number_width(snapshot, window, cx);
|
||||
let indicator_width = dbg!(line_number_width) + scale_px(HORIZONTAL_OFFSET);
|
||||
|
||||
// Debug indicator dimensions
|
||||
dbg!(horizontal_offset);
|
||||
dbg!(vertical_offset);
|
||||
dbg!(indicator_height);
|
||||
dbg!(indicator_width);
|
||||
|
||||
let is_disabled = breakpoint.is_disabled();
|
||||
let breakpoint_arc = Arc::from(breakpoint.clone());
|
||||
|
||||
let (is_hovered, collides_with_existing) = editor.gutter_breakpoint_indicator.0.map_or(
|
||||
(false, false),
|
||||
|PhantomBreakpointIndicator {
|
||||
is_active,
|
||||
display_row,
|
||||
collides_with_existing_breakpoint,
|
||||
}| {
|
||||
(
|
||||
is_active && display_row == row,
|
||||
collides_with_existing_breakpoint,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let indicator_color = if is_hovered {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
} else if is_disabled {
|
||||
cx.theme().status().info.alpha(0.64)
|
||||
} else {
|
||||
cx.theme().status().info.alpha(0.48)
|
||||
};
|
||||
|
||||
let primary_action = if is_disabled {
|
||||
"enable"
|
||||
} else if is_hovered && !collides_with_existing {
|
||||
"set"
|
||||
} else {
|
||||
"unset"
|
||||
};
|
||||
|
||||
let mut tooltip_text = format!("Click to {primary_action}");
|
||||
|
||||
if collides_with_existing && !is_disabled {
|
||||
use std::fmt::Write;
|
||||
let modifier_key = gpui::Keystroke {
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
..Default::default()
|
||||
};
|
||||
write!(tooltip_text, ", {modifier_key}-click to disable").ok();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(element_id)
|
||||
.cursor_pointer()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.w(indicator_width)
|
||||
.h(indicator_height)
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, _cx, _style| {},
|
||||
move |bounds, _cx, window, _style| {
|
||||
// Debug canvas bounds
|
||||
dbg!(&bounds);
|
||||
|
||||
// Adjust bounds to account for horizontal offset
|
||||
let adjusted_bounds = Bounds {
|
||||
origin: point(bounds.origin.x + horizontal_offset, bounds.origin.y),
|
||||
size: size(bounds.size.width, indicator_height),
|
||||
};
|
||||
|
||||
// Debug adjusted bounds
|
||||
dbg!(&adjusted_bounds);
|
||||
dbg!(font_scale);
|
||||
|
||||
// Generate the breakpoint indicator path
|
||||
let path =
|
||||
breakpoint_indicator_path(adjusted_bounds, font_scale, is_disabled);
|
||||
|
||||
// Paint the path with the calculated color
|
||||
window.paint_path(path, indicator_color);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.on_click({
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let breakpoint = breakpoint_arc.clone();
|
||||
move |event, window, cx| {
|
||||
let action = if event.modifiers().platform || breakpoint.is_disabled() {
|
||||
BreakpointEditAction::InvertState
|
||||
} else {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.focus(&editor_strong.focus_handle(cx));
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
action,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_mouse_down(gpui::MouseButton::Right, {
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let anchor_position = position.clone();
|
||||
move |event, window, cx| {
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(anchor_position.clone()),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
@@ -3223,7 +3034,7 @@ impl EditorElement {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: &[RowInfo],
|
||||
_active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -3279,7 +3090,16 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let color = cx.theme().colors().editor_active_line_number;
|
||||
let color = active_rows
|
||||
.get(&display_row)
|
||||
.map(|spec| {
|
||||
if spec.breakpoint {
|
||||
cx.theme().colors().debugger_accent
|
||||
} else {
|
||||
cx.theme().colors().editor_active_line_number
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||||
let shaped_line =
|
||||
self.shape_line_number(SharedString::from(&line_number), color, window);
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
@@ -5757,9 +5577,6 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
window.with_element_namespace("crease_toggles", |window| {
|
||||
for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
|
||||
crease_toggle.paint(window, cx);
|
||||
@@ -5772,23 +5589,12 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5813,6 +5619,20 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
@@ -7059,8 +6879,7 @@ impl EditorElement {
|
||||
layout.width
|
||||
}
|
||||
|
||||
/// Get the width of the longest line number in the current editor in Pixels
|
||||
pub(crate) fn max_line_number_width(
|
||||
fn max_line_number_width(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -7155,7 +6974,7 @@ impl AcceptEditPredictionBinding {
|
||||
}
|
||||
|
||||
fn prepaint_gutter_button(
|
||||
button: impl IntoElement,
|
||||
button: IconButton,
|
||||
row: DisplayRow,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
@@ -7300,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
|
||||
enum LineFragment {
|
||||
Text(ShapedLine),
|
||||
Element {
|
||||
id: FoldId,
|
||||
id: ChunkRendererId,
|
||||
element: Option<AnyElement>,
|
||||
size: Size<Pixels>,
|
||||
len: usize,
|
||||
@@ -8478,7 +8297,7 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let new_fold_widths = line_layouts
|
||||
let new_renrerer_widths = line_layouts
|
||||
.iter()
|
||||
.flat_map(|layout| &layout.fragments)
|
||||
.filter_map(|fragment| {
|
||||
@@ -8489,7 +8308,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
});
|
||||
if self.editor.update(cx, |editor, cx| {
|
||||
editor.update_fold_widths(new_fold_widths, cx)
|
||||
editor.update_renderer_widths(new_renrerer_widths, cx)
|
||||
}) {
|
||||
// If the fold widths have changed, we need to prepaint
|
||||
// the element again to account for any changes in
|
||||
@@ -9140,10 +8959,6 @@ impl Element for EditorElement {
|
||||
self.paint_background(layout, window, cx);
|
||||
self.paint_indent_guides(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_blamed_display_rows(layout, window, cx);
|
||||
self.paint_line_numbers(layout, window, cx);
|
||||
@@ -9151,6 +8966,11 @@ impl Element for EditorElement {
|
||||
|
||||
self.paint_text(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.paint_blocks(layout, window, cx);
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
use gpui::{Bounds, Path, PathBuilder, PathStyle, StrokeOptions, point};
|
||||
use ui::{Pixels, px};
|
||||
|
||||
/// Draw the path for the breakpoint indicator.
|
||||
///
|
||||
/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
|
||||
/// wide to draw without graphical issues, so it will ignore narrower width.
|
||||
pub(crate) fn breakpoint_indicator_path(
|
||||
bounds: Bounds<Pixels>,
|
||||
scale: f32,
|
||||
stroke: bool,
|
||||
) -> Path<Pixels> {
|
||||
// Constants for the breakpoint shape dimensions
|
||||
// The shape is designed based on a 50px wide by 15px high template
|
||||
// and uses 9-slice style scaling to allow the shape to be stretched
|
||||
// vertically and horizontally.
|
||||
const SHAPE_BASE_HEIGHT: f32 = 15.0;
|
||||
const SHAPE_FIXED_WIDTH: f32 = 32.0; // Width of non-stretchable parts (corners)
|
||||
const SHAPE_MIN_WIDTH: f32 = 34.0; // Minimum width to render properly
|
||||
const PIXEL_ROUNDING_FACTOR: f32 = 8.0; // Round to nearest 1/8 pixel
|
||||
|
||||
// Key points in the shape (in base coordinates)
|
||||
const CORNER_RADIUS: f32 = 5.0;
|
||||
const CENTER_Y: f32 = 7.5;
|
||||
const TOP_Y: f32 = 0.0;
|
||||
const BOTTOM_Y: f32 = 15.0;
|
||||
const CURVE_CONTROL_OFFSET: f32 = 1.5;
|
||||
const RIGHT_CORNER_START: f32 = 4.0;
|
||||
const RIGHT_CORNER_WIDTH: f32 = 13.0;
|
||||
|
||||
// Helper function to round pixels to nearest 1/8
|
||||
let round_to_pixel_grid = |value: Pixels| -> Pixels {
|
||||
let value_f32: f32 = value.into();
|
||||
px((value_f32 * PIXEL_ROUNDING_FACTOR).round() / PIXEL_ROUNDING_FACTOR)
|
||||
};
|
||||
|
||||
// Calculate actual dimensions with scaling
|
||||
let min_allowed_width = px(SHAPE_MIN_WIDTH * scale);
|
||||
let actual_width = if bounds.size.width < min_allowed_width {
|
||||
min_allowed_width
|
||||
} else {
|
||||
bounds.size.width
|
||||
};
|
||||
let actual_height = bounds.size.height;
|
||||
|
||||
// Debug input parameters and initial calculations
|
||||
dbg!(&bounds);
|
||||
dbg!(scale);
|
||||
dbg!(stroke);
|
||||
dbg!(min_allowed_width);
|
||||
dbg!(actual_width);
|
||||
dbg!(actual_height);
|
||||
|
||||
// Origin point for positioning
|
||||
let origin_x = bounds.origin.x;
|
||||
let origin_y = bounds.origin.y;
|
||||
|
||||
// Calculate the scale factor based on height and user scale
|
||||
let shape_scale = (actual_height / px(SHAPE_BASE_HEIGHT)) * scale;
|
||||
|
||||
// Calculate the width of fixed and stretchable sections
|
||||
let fixed_sections_width = px(SHAPE_FIXED_WIDTH) * shape_scale;
|
||||
let stretchable_middle_width = actual_width - fixed_sections_width;
|
||||
|
||||
// Debug scaling calculations
|
||||
dbg!(shape_scale);
|
||||
dbg!(fixed_sections_width);
|
||||
dbg!(stretchable_middle_width);
|
||||
|
||||
// Pre-calculate all the key x-coordinates
|
||||
let left_edge_x = round_to_pixel_grid(origin_x);
|
||||
let left_corner_end_x = round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale);
|
||||
let middle_section_end_x =
|
||||
round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale + stretchable_middle_width);
|
||||
let right_corner_start_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_START) * shape_scale,
|
||||
);
|
||||
let right_edge_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_WIDTH) * shape_scale,
|
||||
);
|
||||
|
||||
// Debug x-coordinates
|
||||
dbg!(origin_x);
|
||||
dbg!(left_edge_x);
|
||||
dbg!(left_corner_end_x);
|
||||
dbg!(middle_section_end_x);
|
||||
dbg!(right_corner_start_x);
|
||||
dbg!(right_edge_x);
|
||||
|
||||
// Pre-calculate all the key y-coordinates
|
||||
let top_edge_y = round_to_pixel_grid(origin_y);
|
||||
let center_y = round_to_pixel_grid(origin_y + px(CENTER_Y) * shape_scale);
|
||||
let bottom_edge_y = round_to_pixel_grid(origin_y + px(BOTTOM_Y) * shape_scale);
|
||||
|
||||
// Y-coordinates for the left side curves
|
||||
let left_upper_curve_start_y = round_to_pixel_grid(origin_y + px(CORNER_RADIUS) * shape_scale);
|
||||
let left_lower_curve_end_y = round_to_pixel_grid(origin_y + px(10.0) * shape_scale);
|
||||
|
||||
// Y-coordinates for the right side curves
|
||||
let right_upper_curve_control_y = round_to_pixel_grid(origin_y + px(6.0) * shape_scale);
|
||||
let right_lower_curve_control_y = round_to_pixel_grid(origin_y + px(9.0) * shape_scale);
|
||||
|
||||
// Control point offsets
|
||||
let control_offset = px(CURVE_CONTROL_OFFSET) * shape_scale;
|
||||
let right_control_offset = px(9.0) * shape_scale;
|
||||
|
||||
// Debug y-coordinates
|
||||
dbg!(origin_y);
|
||||
dbg!(top_edge_y);
|
||||
dbg!(center_y);
|
||||
dbg!(bottom_edge_y);
|
||||
dbg!(left_upper_curve_start_y);
|
||||
dbg!(left_lower_curve_end_y);
|
||||
dbg!(right_upper_curve_control_y);
|
||||
dbg!(right_lower_curve_control_y);
|
||||
|
||||
// Create the path builder
|
||||
let mut builder = if stroke {
|
||||
let stroke_width = px(1.0 * scale);
|
||||
let options = StrokeOptions::default().with_line_width(stroke_width.0);
|
||||
PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
|
||||
} else {
|
||||
PathBuilder::fill()
|
||||
};
|
||||
|
||||
// Build the path - starting from left center
|
||||
builder.move_to(point(left_edge_x, center_y));
|
||||
|
||||
// === Upper half of the shape ===
|
||||
|
||||
// Move up to start of left upper curve
|
||||
builder.line_to(point(left_edge_x, left_upper_curve_start_y));
|
||||
|
||||
// Top-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_corner_end_x, top_edge_y),
|
||||
point(left_edge_x, round_to_pixel_grid(origin_y + control_offset)),
|
||||
point(round_to_pixel_grid(origin_x + control_offset), top_edge_y),
|
||||
);
|
||||
|
||||
// Top edge - stretchable middle section
|
||||
builder.line_to(point(middle_section_end_x, top_edge_y));
|
||||
|
||||
// Top edge - right corner start
|
||||
builder.line_to(point(right_corner_start_x, top_edge_y));
|
||||
|
||||
// Top-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_edge_x, center_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
top_edge_y,
|
||||
),
|
||||
point(right_edge_x, right_upper_curve_control_y),
|
||||
);
|
||||
|
||||
// === Lower half of the shape (mirrored) ===
|
||||
|
||||
// Bottom-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_corner_start_x, bottom_edge_y),
|
||||
point(right_edge_x, right_lower_curve_control_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
bottom_edge_y,
|
||||
),
|
||||
);
|
||||
|
||||
// Bottom edge - right corner to middle
|
||||
builder.line_to(point(middle_section_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom edge - stretchable middle section
|
||||
builder.line_to(point(left_corner_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_edge_x, left_lower_curve_end_y),
|
||||
point(
|
||||
round_to_pixel_grid(origin_x + control_offset),
|
||||
bottom_edge_y,
|
||||
),
|
||||
point(
|
||||
left_edge_x,
|
||||
round_to_pixel_grid(origin_y + px(13.5) * shape_scale),
|
||||
),
|
||||
);
|
||||
|
||||
// Close the path by returning to start
|
||||
builder.line_to(point(left_edge_x, center_y));
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod breakpoint_indicator;
|
||||
@@ -19,18 +19,21 @@ use crate::{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LspColorData {
|
||||
cache_version_used: usize,
|
||||
buffer_colors: HashMap<BufferId, BufferColors>,
|
||||
render_mode: DocumentColorsRenderMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct BufferColors {
|
||||
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
|
||||
inlay_colors: HashMap<InlayId, usize>,
|
||||
render_mode: DocumentColorsRenderMode,
|
||||
cache_version_used: usize,
|
||||
}
|
||||
|
||||
impl LspColorData {
|
||||
pub fn new(cx: &App) -> Self {
|
||||
Self {
|
||||
cache_version_used: 0,
|
||||
colors: Vec::new(),
|
||||
inlay_colors: HashMap::default(),
|
||||
buffer_colors: HashMap::default(),
|
||||
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
|
||||
}
|
||||
}
|
||||
@@ -47,8 +50,9 @@ impl LspColorData {
|
||||
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
|
||||
to_remove: Vec::new(),
|
||||
to_insert: self
|
||||
.colors
|
||||
.buffer_colors
|
||||
.iter()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
|
||||
.map(|(range, color, id)| {
|
||||
Inlay::color(
|
||||
id.id(),
|
||||
@@ -63,33 +67,49 @@ impl LspColorData {
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
DocumentColorsRenderMode::None => {
|
||||
self.colors.clear();
|
||||
Some(InlaySplice {
|
||||
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
|
||||
to_insert: Vec::new(),
|
||||
})
|
||||
}
|
||||
DocumentColorsRenderMode::None => Some(InlaySplice {
|
||||
to_remove: self
|
||||
.buffer_colors
|
||||
.drain()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
to_insert: Vec::new(),
|
||||
}),
|
||||
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
|
||||
Some(InlaySplice {
|
||||
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
|
||||
to_remove: self
|
||||
.buffer_colors
|
||||
.iter_mut()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
to_insert: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
|
||||
if self.colors == colors {
|
||||
fn set_colors(
|
||||
&mut self,
|
||||
buffer_id: BufferId,
|
||||
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
|
||||
cache_version: Option<usize>,
|
||||
) -> bool {
|
||||
let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
|
||||
if let Some(cache_version) = cache_version {
|
||||
buffer_colors.cache_version_used = cache_version;
|
||||
}
|
||||
if buffer_colors.colors == colors {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.inlay_colors = colors
|
||||
buffer_colors.inlay_colors = colors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (_, _, id))| (*id, i))
|
||||
.collect();
|
||||
self.colors = colors;
|
||||
buffer_colors.colors = colors;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -103,8 +123,9 @@ impl LspColorData {
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
self.colors
|
||||
self.buffer_colors
|
||||
.iter()
|
||||
.flat_map(|(_, buffer_colors)| &buffer_colors.colors)
|
||||
.map(|(range, color, _)| {
|
||||
let display_range = range.clone().to_display_points(snapshot);
|
||||
let color = Hsla::from(Rgba {
|
||||
@@ -162,10 +183,9 @@ impl Editor {
|
||||
ColorFetchStrategy::IgnoreCache
|
||||
} else {
|
||||
ColorFetchStrategy::UseCache {
|
||||
known_cache_version: self
|
||||
.colors
|
||||
.as_ref()
|
||||
.map(|colors| colors.cache_version_used),
|
||||
known_cache_version: self.colors.as_ref().and_then(|colors| {
|
||||
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
|
||||
}),
|
||||
}
|
||||
};
|
||||
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
|
||||
@@ -201,15 +221,13 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut cache_version = None;
|
||||
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
|
||||
let mut new_editor_colors = HashMap::default();
|
||||
for (buffer_id, colors) in all_colors {
|
||||
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
match colors {
|
||||
Ok(colors) => {
|
||||
cache_version = colors.cache_version;
|
||||
for color in colors.colors {
|
||||
let color_start = point_from_lsp(color.lsp_range.start);
|
||||
let color_end = point_from_lsp(color.lsp_range.end);
|
||||
@@ -243,8 +261,15 @@ impl Editor {
|
||||
continue;
|
||||
};
|
||||
|
||||
let new_entry =
|
||||
new_editor_colors.entry(buffer_id).or_insert_with(|| {
|
||||
(Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
|
||||
});
|
||||
new_entry.1 = colors.cache_version;
|
||||
let new_buffer_colors = &mut new_entry.0;
|
||||
|
||||
let (Ok(i) | Err(i)) =
|
||||
new_editor_colors.binary_search_by(|(probe, _)| {
|
||||
new_buffer_colors.binary_search_by(|(probe, _)| {
|
||||
probe
|
||||
.start
|
||||
.cmp(&color_start_anchor, &multi_buffer_snapshot)
|
||||
@@ -254,7 +279,7 @@ impl Editor {
|
||||
.cmp(&color_end_anchor, &multi_buffer_snapshot)
|
||||
})
|
||||
});
|
||||
new_editor_colors
|
||||
new_buffer_colors
|
||||
.insert(i, (color_start_anchor..color_end_anchor, color));
|
||||
break;
|
||||
}
|
||||
@@ -267,45 +292,70 @@ impl Editor {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let mut colors_splice = InlaySplice::default();
|
||||
let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
|
||||
let Some(colors) = &mut editor.colors else {
|
||||
return;
|
||||
};
|
||||
let mut existing_colors = colors.colors.iter().peekable();
|
||||
for (new_range, new_color) in new_editor_colors {
|
||||
let rgba_color = Rgba {
|
||||
r: new_color.color.red,
|
||||
g: new_color.color.green,
|
||||
b: new_color.color.blue,
|
||||
a: new_color.color.alpha,
|
||||
};
|
||||
let mut updated = false;
|
||||
for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
|
||||
let mut new_buffer_color_inlays =
|
||||
Vec::with_capacity(new_buffer_colors.len());
|
||||
let mut existing_buffer_colors = colors
|
||||
.buffer_colors
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.colors
|
||||
.iter()
|
||||
.peekable();
|
||||
for (new_range, new_color) in new_buffer_colors {
|
||||
let rgba_color = Rgba {
|
||||
r: new_color.color.red,
|
||||
g: new_color.color.green,
|
||||
b: new_color.color.blue,
|
||||
a: new_color.color.alpha,
|
||||
};
|
||||
|
||||
loop {
|
||||
match existing_colors.peek() {
|
||||
Some((existing_range, existing_color, existing_inlay_id)) => {
|
||||
match existing_range
|
||||
.start
|
||||
.cmp(&new_range.start, &multi_buffer_snapshot)
|
||||
.then_with(|| {
|
||||
existing_range
|
||||
.end
|
||||
.cmp(&new_range.end, &multi_buffer_snapshot)
|
||||
}) {
|
||||
cmp::Ordering::Less => {
|
||||
colors_splice.to_remove.push(*existing_inlay_id);
|
||||
existing_colors.next();
|
||||
continue;
|
||||
}
|
||||
cmp::Ordering::Equal => {
|
||||
if existing_color == &new_color {
|
||||
new_color_inlays.push((
|
||||
new_range,
|
||||
new_color,
|
||||
*existing_inlay_id,
|
||||
));
|
||||
} else {
|
||||
loop {
|
||||
match existing_buffer_colors.peek() {
|
||||
Some((existing_range, existing_color, existing_inlay_id)) => {
|
||||
match existing_range
|
||||
.start
|
||||
.cmp(&new_range.start, &multi_buffer_snapshot)
|
||||
.then_with(|| {
|
||||
existing_range
|
||||
.end
|
||||
.cmp(&new_range.end, &multi_buffer_snapshot)
|
||||
}) {
|
||||
cmp::Ordering::Less => {
|
||||
colors_splice.to_remove.push(*existing_inlay_id);
|
||||
existing_buffer_colors.next();
|
||||
continue;
|
||||
}
|
||||
cmp::Ordering::Equal => {
|
||||
if existing_color == &new_color {
|
||||
new_buffer_color_inlays.push((
|
||||
new_range,
|
||||
new_color,
|
||||
*existing_inlay_id,
|
||||
));
|
||||
} else {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.push(*existing_inlay_id);
|
||||
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
}
|
||||
existing_buffer_colors.next();
|
||||
break;
|
||||
}
|
||||
cmp::Ordering::Greater => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
@@ -313,49 +363,40 @@ impl Editor {
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
existing_colors.next();
|
||||
break;
|
||||
}
|
||||
cmp::Ordering::Greater => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
None => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if existing_colors.peek().is_some() {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.extend(existing_colors.map(|(_, _, id)| *id));
|
||||
|
||||
if existing_buffer_colors.peek().is_some() {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.extend(existing_buffer_colors.map(|(_, _, id)| *id));
|
||||
}
|
||||
updated |= colors.set_colors(
|
||||
buffer_id,
|
||||
new_buffer_color_inlays,
|
||||
new_cache_version,
|
||||
);
|
||||
}
|
||||
|
||||
let mut updated = colors.set_colors(new_color_inlays);
|
||||
if let Some(cache_version) = cache_version {
|
||||
colors.cache_version_used = cache_version;
|
||||
}
|
||||
if colors.render_mode == DocumentColorsRenderMode::Inlay
|
||||
&& (!colors_splice.to_insert.is_empty()
|
||||
|| !colors_splice.to_remove.is_empty())
|
||||
|
||||
@@ -32,7 +32,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
debug_adapter_extension.workspace = true
|
||||
dirs.workspace = true
|
||||
dotenv.workspace = true
|
||||
dotenvy.workspace = true
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -63,7 +63,7 @@ struct Args {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dotenv::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
|
||||
dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
|
||||
|
||||
env_logger::init();
|
||||
|
||||
|
||||
@@ -1054,6 +1054,15 @@ pub fn response_events_to_markdown(
|
||||
| LanguageModelCompletionEvent::StartMessage { .. }
|
||||
| LanguageModelCompletionEvent::StatusUpdate { .. },
|
||||
) => {}
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
json_parse_error, ..
|
||||
}) => {
|
||||
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
|
||||
response.push_str(&format!(
|
||||
"**Error**: parse error in tool use JSON: {}\n\n",
|
||||
json_parse_error
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
|
||||
response.push_str(&format!("**Error**: {}\n\n", error));
|
||||
@@ -1132,6 +1141,17 @@ impl ThreadDialog {
|
||||
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
|
||||
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}
|
||||
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
json_parse_error,
|
||||
..
|
||||
}) => {
|
||||
flush_text(&mut current_text, &mut content);
|
||||
content.push(MessageContent::Text(format!(
|
||||
"ERROR: parse error in tool use JSON: {}",
|
||||
json_parse_error
|
||||
)));
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
flush_text(&mut current_text, &mut content);
|
||||
content.push(MessageContent::Text(format!("ERROR: {}", error)));
|
||||
|
||||
@@ -33,7 +33,7 @@ interface dap {
|
||||
}
|
||||
|
||||
/// Debug Config is the "highest-level" configuration for a debug session.
|
||||
/// It comes from a new session modal UI; thus, it is essentially debug-adapter-agnostic.
|
||||
/// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic.
|
||||
/// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario).
|
||||
record debug-config {
|
||||
/// Name of the debug task
|
||||
|
||||
@@ -70,6 +70,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("templ", &["templ"]),
|
||||
("terraform", &["tf", "tfvars", "hcl"]),
|
||||
("toml", &["Cargo.lock", "toml"]),
|
||||
("typst", &["typ"]),
|
||||
("vue", &["vue"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("wit", &["wit"]),
|
||||
|
||||
@@ -65,6 +65,7 @@ actions!(
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RestoreFile {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
|
||||
@@ -388,6 +388,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_collaboration_hub(Box::new(project));
|
||||
commit_editor.set_use_autoclose(false);
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
commit_editor.set_use_modal_editing(true);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
|
||||
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["http_client", "font-kit", "wayland", "x11"]
|
||||
default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"leak-detection",
|
||||
"collections/test-support",
|
||||
@@ -69,7 +69,7 @@ x11 = [
|
||||
"open",
|
||||
"scap",
|
||||
]
|
||||
|
||||
windows-manifest = []
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
|
||||
@@ -17,7 +17,7 @@ fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
|
||||
Ok("windows") => {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
|
||||
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, bounds,
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
|
||||
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
|
||||
div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
struct PaintingViewer {
|
||||
@@ -150,14 +150,6 @@ impl PaintingViewer {
|
||||
let path = builder.build().unwrap();
|
||||
lines.push((path, gpui::green().into()));
|
||||
|
||||
// draw the indicators (aligned and unaligned versions)
|
||||
let aligned_indicator = breakpoint_indicator_path(
|
||||
bounds(point(px(50.), px(250.)), size(px(60.), px(16.))),
|
||||
1.0,
|
||||
false,
|
||||
);
|
||||
lines.push((aligned_indicator, rgb(0x1e88e5).into()));
|
||||
|
||||
Self {
|
||||
default_lines: lines.clone(),
|
||||
lines: vec![],
|
||||
@@ -314,137 +306,3 @@ fn main() {
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw the path for the breakpoint indicator.
|
||||
///
|
||||
/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
|
||||
/// wide to draw without graphical issues, so it will ignore narrower width.
|
||||
fn breakpoint_indicator_path(bounds: Bounds<Pixels>, scale: f32, stroke: bool) -> Path<Pixels> {
|
||||
static MIN_WIDTH: f32 = 31.;
|
||||
|
||||
// Apply user scale to the minimum width
|
||||
let min_width = MIN_WIDTH * scale;
|
||||
|
||||
let width = if bounds.size.width.0 < min_width {
|
||||
px(min_width)
|
||||
} else {
|
||||
bounds.size.width
|
||||
};
|
||||
let height = bounds.size.height;
|
||||
|
||||
// Position the indicator on the canvas
|
||||
let base_x = bounds.origin.x;
|
||||
let base_y = bounds.origin.y;
|
||||
|
||||
// Calculate the scaling factor for the height (SVG is 15px tall), incorporating user scale
|
||||
let scale_factor = (height / px(15.0)) * scale;
|
||||
|
||||
// Calculate how much width to allocate to the stretchable middle section
|
||||
// SVG has 32px of fixed elements (corners), so the rest is for the middle
|
||||
let fixed_width = px(32.0) * scale_factor;
|
||||
let middle_width = width - fixed_width;
|
||||
|
||||
// Helper function to round to nearest quarter pixel
|
||||
let round_to_quarter = |value: Pixels| -> Pixels {
|
||||
let value_f32: f32 = value.into();
|
||||
px((value_f32 * 4.0).round() / 4.0)
|
||||
};
|
||||
|
||||
// Create a new path - either fill or stroke based on the flag
|
||||
let mut builder = if stroke {
|
||||
// For stroke, we need to set appropriate line width and options
|
||||
let stroke_width = px(1.0 * scale); // Apply scale to stroke width
|
||||
let options = StrokeOptions::default().with_line_width(stroke_width.0);
|
||||
|
||||
PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
|
||||
} else {
|
||||
// For fill, use the original implementation
|
||||
PathBuilder::fill()
|
||||
};
|
||||
|
||||
// Upper half of the shape - Based on the provided SVG
|
||||
// Start at bottom left (0, 8)
|
||||
let start_x = round_to_quarter(base_x);
|
||||
let start_y = round_to_quarter(base_y + px(7.5) * scale_factor);
|
||||
builder.move_to(point(start_x, start_y));
|
||||
|
||||
// Vertical line to (0, 5)
|
||||
let vert_y = round_to_quarter(base_y + px(5.0) * scale_factor);
|
||||
builder.line_to(point(start_x, vert_y));
|
||||
|
||||
// Curve to (5, 0) - using cubic Bezier
|
||||
let curve1_end_x = round_to_quarter(base_x + px(5.0) * scale_factor);
|
||||
let curve1_end_y = round_to_quarter(base_y);
|
||||
let curve1_ctrl1_x = round_to_quarter(base_x);
|
||||
let curve1_ctrl1_y = round_to_quarter(base_y + px(1.5) * scale_factor);
|
||||
let curve1_ctrl2_x = round_to_quarter(base_x + px(1.5) * scale_factor);
|
||||
let curve1_ctrl2_y = round_to_quarter(base_y);
|
||||
builder.cubic_bezier_to(
|
||||
point(curve1_end_x, curve1_end_y),
|
||||
point(curve1_ctrl1_x, curve1_ctrl1_y),
|
||||
point(curve1_ctrl2_x, curve1_ctrl2_y),
|
||||
);
|
||||
|
||||
// Horizontal line through the middle section to (37, 0)
|
||||
let middle_end_x = round_to_quarter(base_x + px(5.0) * scale_factor + middle_width);
|
||||
builder.line_to(point(middle_end_x, curve1_end_y));
|
||||
|
||||
// Horizontal line to (41, 0)
|
||||
let right_section_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(4.0) * scale_factor);
|
||||
builder.line_to(point(right_section_x, curve1_end_y));
|
||||
|
||||
// Curve to (50, 7.5) - using cubic Bezier
|
||||
let curve2_end_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve2_end_y = round_to_quarter(base_y + px(7.5) * scale_factor);
|
||||
let curve2_ctrl1_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
|
||||
let curve2_ctrl1_y = round_to_quarter(base_y);
|
||||
let curve2_ctrl2_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve2_ctrl2_y = round_to_quarter(base_y + px(6.0) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(curve2_end_x, curve2_end_y),
|
||||
point(curve2_ctrl1_x, curve2_ctrl1_y),
|
||||
point(curve2_ctrl2_x, curve2_ctrl2_y),
|
||||
);
|
||||
|
||||
// Lower half of the shape - mirrored vertically
|
||||
// Curve from (50, 7.5) to (41, 15)
|
||||
let curve3_end_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
let curve3_ctrl1_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(13.0) * scale_factor);
|
||||
let curve3_ctrl1_y = round_to_quarter(base_y + px(9.0) * scale_factor);
|
||||
let curve3_ctrl2_x =
|
||||
round_to_quarter(base_x + px(5.0) * scale_factor + middle_width + px(9.0) * scale_factor);
|
||||
let curve3_ctrl2_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(right_section_x, curve3_end_y),
|
||||
point(curve3_ctrl1_x, curve3_ctrl1_y),
|
||||
point(curve3_ctrl2_x, curve3_ctrl2_y),
|
||||
);
|
||||
|
||||
// Horizontal line to (37, 15)
|
||||
builder.line_to(point(middle_end_x, curve3_end_y));
|
||||
|
||||
// Horizontal line through the middle section to (5, 15)
|
||||
builder.line_to(point(curve1_end_x, curve3_end_y));
|
||||
|
||||
// Curve to (0, 10)
|
||||
let curve4_end_y = round_to_quarter(base_y + px(10.0) * scale_factor);
|
||||
let curve4_ctrl1_x = round_to_quarter(base_x + px(1.5) * scale_factor);
|
||||
let curve4_ctrl1_y = round_to_quarter(base_y + px(15.0) * scale_factor);
|
||||
let curve4_ctrl2_x = round_to_quarter(base_x);
|
||||
let curve4_ctrl2_y = round_to_quarter(base_y + px(13.5) * scale_factor);
|
||||
builder.cubic_bezier_to(
|
||||
point(start_x, curve4_end_y),
|
||||
point(curve4_ctrl1_x, curve4_ctrl1_y),
|
||||
point(curve4_ctrl2_x, curve4_ctrl2_y),
|
||||
);
|
||||
|
||||
// Close the path
|
||||
builder.line_to(point(start_x, start_y));
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
@@ -125,9 +125,7 @@ pub trait Action: Any + Send {
|
||||
Self: Sized;
|
||||
|
||||
/// Optional JSON schema for the action's input data.
|
||||
fn action_json_schema(
|
||||
_: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Option<schemars::schema::Schema>
|
||||
fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -238,7 +236,7 @@ impl Default for ActionRegistry {
|
||||
|
||||
struct ActionData {
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
}
|
||||
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
@@ -253,7 +251,7 @@ pub struct MacroActionData {
|
||||
pub name: &'static str,
|
||||
pub type_id: TypeId,
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
pub deprecated_aliases: &'static [&'static str],
|
||||
pub deprecation_message: Option<&'static str>,
|
||||
}
|
||||
@@ -357,8 +355,8 @@ impl ActionRegistry {
|
||||
|
||||
pub fn action_schemas(
|
||||
&self,
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::Schema>)> {
|
||||
// Use the order from all_names so that the resulting schema has sensible order.
|
||||
self.all_names
|
||||
.iter()
|
||||
|
||||
@@ -1334,6 +1334,11 @@ impl App {
|
||||
self.pending_effects.push_back(Effect::RefreshWindows);
|
||||
}
|
||||
|
||||
/// Get all key bindings in the app.
|
||||
pub fn key_bindings(&self) -> Rc<RefCell<Keymap>> {
|
||||
self.keymap.clone()
|
||||
}
|
||||
|
||||
/// Register a global listener for actions invoked via the keyboard.
|
||||
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
|
||||
self.global_action_listeners
|
||||
@@ -1388,8 +1393,8 @@ impl App {
|
||||
/// Get all non-internal actions that have been registered, along with their schemas.
|
||||
pub fn action_schemas(
|
||||
&self,
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::Schema>)> {
|
||||
self.actions.action_schemas(generator)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{self, Visitor},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
hash::{Hash, Hasher},
|
||||
@@ -99,22 +100,14 @@ impl Visitor<'_> for RgbaVisitor {
|
||||
}
|
||||
|
||||
impl JsonSchema for Rgba {
|
||||
fn schema_name() -> String {
|
||||
"Rgba".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Rgba".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(
|
||||
r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -629,11 +622,11 @@ impl From<Rgba> for Hsla {
|
||||
}
|
||||
|
||||
impl JsonSchema for Hsla {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Rgba::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
Rgba::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,10 +613,10 @@ pub trait InteractiveElement: Sized {
|
||||
/// Track the focus state of the given focus handle on this element.
|
||||
/// If the focus handle is focused by the application, this element will
|
||||
/// apply its focused styles.
|
||||
fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
|
||||
fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
|
||||
FocusableWrapper { element: self }
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the keymap context for this element. This will be used to determine
|
||||
@@ -980,15 +980,35 @@ pub trait InteractiveElement: Sized {
|
||||
self.interactivity().block_mouse_except_scroll();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element, specifically, is focused.
|
||||
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
|
||||
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element is inside another element that is focused.
|
||||
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
|
||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI interactivity features
|
||||
/// that require state.
|
||||
pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
/// Set this element to focusable.
|
||||
fn focusable(mut self) -> FocusableWrapper<Self> {
|
||||
fn focusable(mut self) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
FocusableWrapper { element: self }
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overflow x and y to scroll.
|
||||
@@ -1118,27 +1138,6 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for providing focus related APIs to interactive elements
|
||||
pub trait FocusableElement: InteractiveElement {
|
||||
/// Set the given styles to be applied when this element, specifically, is focused.
|
||||
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element is inside another element that is focused.
|
||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type MouseDownListener =
|
||||
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
pub(crate) type MouseUpListener =
|
||||
@@ -2777,126 +2776,6 @@ impl GroupHitboxes {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an element that can be focused.
|
||||
pub struct FocusableWrapper<E> {
|
||||
/// The element that is focusable
|
||||
pub element: E,
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
|
||||
|
||||
impl<E> InteractiveElement for FocusableWrapper<E>
|
||||
where
|
||||
E: InteractiveElement,
|
||||
{
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
|
||||
|
||||
impl<E> Styled for FocusableWrapper<E>
|
||||
where
|
||||
E: Styled,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.element.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableWrapper<Div> {
|
||||
/// Add a listener to be called when the children of this `Div` are prepainted.
|
||||
/// This allows you to store the [`Bounds`] of the children for later use.
|
||||
pub fn on_children_prepainted(
|
||||
mut self,
|
||||
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.element = self.element.on_children_prepainted(listener);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Element for FocusableWrapper<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
type RequestLayoutState = E::RequestLayoutState;
|
||||
type PrepaintState = E::PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.element.id()
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
self.element.source_location()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(id, inspector_id, window, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> E::PrepaintState {
|
||||
self.element
|
||||
.prepaint(id, inspector_id, bounds, state, window, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.element.paint(
|
||||
id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> IntoElement for FocusableWrapper<E>
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
type Element = E::Element;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self.element.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ParentElement for FocusableWrapper<E>
|
||||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
||||
pub struct Stateful<E> {
|
||||
pub(crate) element: E,
|
||||
@@ -2927,8 +2806,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FocusableElement> FocusableElement for Stateful<E> {}
|
||||
|
||||
impl<E> Element for Stateful<E>
|
||||
where
|
||||
E: Element,
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::{
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
|
||||
use super::{Stateful, StatefulInteractiveElement};
|
||||
|
||||
/// The delay before showing the loading state.
|
||||
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
|
||||
@@ -509,8 +509,6 @@ impl IntoElement for Img {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableElement for Img {}
|
||||
|
||||
impl StatefulInteractiveElement for Img {}
|
||||
|
||||
impl ImageSource {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
|
||||
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
|
||||
Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point,
|
||||
px, size,
|
||||
Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
|
||||
Window, point, px, size,
|
||||
};
|
||||
use collections::VecDeque;
|
||||
use refineable::Refineable as _;
|
||||
@@ -962,12 +962,15 @@ impl Element for List {
|
||||
let height = bounds.size.height;
|
||||
let scroll_top = prepaint.layout.scroll_top;
|
||||
let hitbox_id = prepaint.hitbox.id;
|
||||
let mut accumulated_scroll_delta = ScrollDelta::default();
|
||||
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
|
||||
accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
|
||||
let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
|
||||
list_state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
event.delta.pixel_delta(px(20.)),
|
||||
pixel_delta,
|
||||
current_view,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -6,8 +6,9 @@ use anyhow::{Context as _, anyhow};
|
||||
use core::fmt::Debug;
|
||||
use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
|
||||
use refineable::Refineable;
|
||||
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
cmp::{self, PartialOrd},
|
||||
fmt::{self, Display},
|
||||
@@ -3229,20 +3230,14 @@ impl TryFrom<&'_ str> for AbsoluteLength {
|
||||
}
|
||||
|
||||
impl JsonSchema for AbsoluteLength {
|
||||
fn schema_name() -> String {
|
||||
"AbsoluteLength".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AbsoluteLength".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^-?\d+(\.\d+)?(px|rem)$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3366,20 +3361,14 @@ impl TryFrom<&'_ str> for DefiniteLength {
|
||||
}
|
||||
|
||||
impl JsonSchema for DefiniteLength {
|
||||
fn schema_name() -> String {
|
||||
"DefiniteLength".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DefiniteLength".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^-?\d+(\.\d+)?(px|rem|%)$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3480,20 +3469,14 @@ impl TryFrom<&'_ str> for Length {
|
||||
}
|
||||
|
||||
impl JsonSchema for Length {
|
||||
fn schema_name() -> String {
|
||||
"Length".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Length".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
@@ -11,6 +11,8 @@ pub struct KeyBinding {
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
pub(crate) action_input: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Clone for KeyBinding {
|
||||
@@ -20,6 +22,7 @@ impl Clone for KeyBinding {
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
context_predicate: self.context_predicate.clone(),
|
||||
meta: self.meta,
|
||||
action_input: self.action_input.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +35,7 @@ impl KeyBinding {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
@@ -41,6 +44,7 @@ impl KeyBinding {
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
action_input: Option<SharedString>,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
@@ -62,6 +66,7 @@ impl KeyBinding {
|
||||
action,
|
||||
context_predicate,
|
||||
meta: None,
|
||||
action_input,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +115,11 @@ impl KeyBinding {
|
||||
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
|
||||
self.meta
|
||||
}
|
||||
|
||||
/// Get the action input associated with the action for this binding
|
||||
pub fn action_input(&self) -> Option<SharedString> {
|
||||
self.action_input.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for KeyBinding {
|
||||
|
||||
@@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str {
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(
|
||||
WindowsPlatform::new()
|
||||
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
|
||||
.inspect_err(|err| show_error("Failed to launch", err.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1299,12 +1299,8 @@ mod windows_renderer {
|
||||
size: Default::default(),
|
||||
transparent,
|
||||
};
|
||||
BladeRenderer::new(context, &raw, config).inspect_err(|err| {
|
||||
show_error(
|
||||
"Error: Zed failed to initialize BladeRenderer",
|
||||
err.to_string(),
|
||||
)
|
||||
})
|
||||
BladeRenderer::new(context, &raw, config)
|
||||
.inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
AppContext as _, BorrowAppContext, Context, Element, FocusableElement, InteractiveElement,
|
||||
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
|
||||
StyledImage, VisualContext, util::FluentBuilder,
|
||||
AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement,
|
||||
ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage,
|
||||
VisualContext, util::FluentBuilder,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ use derive_more::{Deref, DerefMut};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Borrow, sync::Arc};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::arc_cow::ArcCow;
|
||||
|
||||
/// A shared string is an immutable string that can be cheaply cloned in GPUI
|
||||
@@ -23,12 +26,16 @@ impl SharedString {
|
||||
}
|
||||
|
||||
impl JsonSchema for SharedString {
|
||||
fn schema_name() -> String {
|
||||
fn inline_schema() -> bool {
|
||||
String::inline_schema()
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
String::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
String::json_schema(r#gen)
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
String::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use schemars::schema::{InstanceType, SchemaObject};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
|
||||
/// The OpenType features that can be configured for a given font.
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -128,36 +129,23 @@ impl serde::Serialize for FontFeatures {
|
||||
}
|
||||
}
|
||||
|
||||
impl schemars::JsonSchema for FontFeatures {
|
||||
fn schema_name() -> String {
|
||||
impl JsonSchema for FontFeatures {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"FontFeatures".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
|
||||
InstanceType::Object,
|
||||
)));
|
||||
{
|
||||
let mut property = SchemaObject {
|
||||
instance_type: Some(schemars::schema::SingleOrVec::Vec(vec![
|
||||
InstanceType::Boolean,
|
||||
InstanceType::Integer,
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
{
|
||||
let mut number_constraints = property.number();
|
||||
number_constraints.multiple_of = Some(1.0);
|
||||
number_constraints.minimum = Some(0.0);
|
||||
}
|
||||
schema
|
||||
.object()
|
||||
.pattern_properties
|
||||
.insert("[0-9a-zA-Z]{4}$".into(), property.into());
|
||||
}
|
||||
schema.into()
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"[0-9a-zA-Z]{4}$": {
|
||||
"type": ["boolean", "integer"],
|
||||
"minimum": 0,
|
||||
"multipleOf": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ fn test_action_macros() {
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = test_only)]
|
||||
struct AnotherSomeAction;
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct AnotherAction;
|
||||
|
||||
#[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RegisterableAction {}
|
||||
|
||||
register_action!(RegisterableAction);
|
||||
|
||||
@@ -159,8 +159,8 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
fn action_json_schema(
|
||||
_generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
_generator: &mut gpui::private::schemars::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::Schema> {
|
||||
#json_schema_fn_body
|
||||
}
|
||||
|
||||
|
||||
@@ -967,6 +967,7 @@ fn toggle_show_inline_completions_for_language(
|
||||
all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
|
||||
file.languages
|
||||
.0
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_edit_predictions = Some(!show_edit_predictions);
|
||||
|
||||
@@ -39,6 +39,7 @@ globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
imara-diff.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
|
||||
@@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
|
||||
#[gpui::test]
|
||||
fn test_autoindent_with_injected_languages(cx: &mut App) {
|
||||
init_settings(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"HTML".into(),
|
||||
LanguageSettingsContent {
|
||||
|
||||
@@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer
|
||||
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
r#gen::SchemaGenerator,
|
||||
schema::{InstanceType, Schema, SchemaObject},
|
||||
};
|
||||
use schemars::{JsonSchema, SchemaGenerator, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
@@ -694,7 +690,6 @@ pub struct LanguageConfig {
|
||||
pub matcher: LanguageMatcher,
|
||||
/// List of bracket types in a language.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "bracket_pair_config_json_schema")]
|
||||
pub brackets: BracketPairConfig,
|
||||
/// If set to true, auto indentation uses last non empty line to determine
|
||||
/// the indentation level for a new line.
|
||||
@@ -735,6 +730,13 @@ pub struct LanguageConfig {
|
||||
/// Starting and closing characters of a block comment.
|
||||
#[serde(default)]
|
||||
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
||||
/// A list of additional regex patterns that should be treated as prefixes
|
||||
/// for creating boundaries during rewrapping, ensuring content from one
|
||||
/// prefixed section doesn't merge with another (e.g., markdown list items).
|
||||
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
|
||||
#[serde(default, deserialize_with = "deserialize_regex_vec")]
|
||||
#[schemars(schema_with = "regex_vec_json_schema")]
|
||||
pub rewrap_prefixes: Vec<Regex>,
|
||||
/// A list of language servers that are allowed to run on subranges of a given language.
|
||||
#[serde(default)]
|
||||
pub scope_opt_in_language_servers: Vec<LanguageServerName>,
|
||||
@@ -914,6 +916,7 @@ impl Default for LanguageConfig {
|
||||
autoclose_before: Default::default(),
|
||||
line_comments: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
rewrap_prefixes: Default::default(),
|
||||
scope_opt_in_language_servers: Default::default(),
|
||||
overrides: Default::default(),
|
||||
word_characters: Default::default(),
|
||||
@@ -944,10 +947,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
|
||||
}
|
||||
}
|
||||
|
||||
fn regex_json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
..Default::default()
|
||||
fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -961,6 +963,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
|
||||
let sources = Vec::<String>::deserialize(d)?;
|
||||
let mut regexes = Vec::new();
|
||||
for source in sources {
|
||||
regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
|
||||
}
|
||||
Ok(regexes)
|
||||
}
|
||||
|
||||
fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLspAdapter {
|
||||
@@ -988,12 +1006,12 @@ pub struct FakeLspAdapter {
|
||||
/// This struct includes settings for defining which pairs of characters are considered brackets and
|
||||
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
|
||||
#[derive(Clone, Debug, Default, JsonSchema)]
|
||||
#[schemars(with = "Vec::<BracketPairContent>")]
|
||||
pub struct BracketPairConfig {
|
||||
/// A list of character pairs that should be treated as brackets in the context of a given language.
|
||||
pub pairs: Vec<BracketPair>,
|
||||
/// A list of tree-sitter scopes for which a given bracket should not be active.
|
||||
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
|
||||
#[serde(skip)]
|
||||
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -1003,10 +1021,6 @@ impl BracketPairConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema {
|
||||
Option::<Vec<BracketPairContent>>::json_schema(r#gen)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct BracketPairContent {
|
||||
#[serde(flatten)]
|
||||
@@ -1841,6 +1855,14 @@ impl LanguageScope {
|
||||
.map(|e| (&e.0, &e.1))
|
||||
}
|
||||
|
||||
/// Returns additional regex patterns that act as prefix markers for creating
|
||||
/// boundaries during rewrapping.
|
||||
///
|
||||
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
|
||||
pub fn rewrap_prefixes(&self) -> &[Regex] {
|
||||
&self.language.config.rewrap_prefixes
|
||||
}
|
||||
|
||||
/// Returns a list of language-specific word characters.
|
||||
///
|
||||
/// By default, Zed treats alphanumeric characters (and '_') as word characters for
|
||||
|
||||
@@ -1170,7 +1170,7 @@ impl LanguageRegistryState {
|
||||
if let Some(theme) = self.theme.as_ref() {
|
||||
language.set_theme(theme.syntax());
|
||||
}
|
||||
self.language_settings.languages.insert(
|
||||
self.language_settings.languages.0.insert(
|
||||
language.name(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: language.config.tab_size,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use crate::{File, Language, LanguageName, LanguageServerName};
|
||||
use anyhow::Result;
|
||||
use collections::{FxHashMap, HashMap, HashSet};
|
||||
use core::slice;
|
||||
use ec4rs::{
|
||||
Properties as EditorconfigProperties,
|
||||
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
|
||||
@@ -11,20 +10,18 @@ use ec4rs::{
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{App, Modifiers};
|
||||
use itertools::{Either, Itertools};
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||
};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use settings::{
|
||||
Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties,
|
||||
ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
|
||||
replace_subschema,
|
||||
};
|
||||
use shellexpand;
|
||||
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
|
||||
use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
|
||||
use util::serde::default_true;
|
||||
|
||||
/// Initializes the language settings.
|
||||
@@ -306,13 +303,42 @@ pub struct AllLanguageSettingsContent {
|
||||
pub defaults: LanguageSettingsContent,
|
||||
/// The settings for individual languages.
|
||||
#[serde(default)]
|
||||
pub languages: HashMap<LanguageName, LanguageSettingsContent>,
|
||||
pub languages: LanguageToSettingsMap,
|
||||
/// Settings for associating file extensions and filenames
|
||||
/// with languages.
|
||||
#[serde(default)]
|
||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
|
||||
/// names in the keys.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
|
||||
|
||||
inventory::submit! {
|
||||
ParameterizedJsonSchema {
|
||||
add_and_get_ref: |generator, params, _cx| {
|
||||
let language_settings_content_ref = generator
|
||||
.subschema_for::<LanguageSettingsContent>()
|
||||
.to_value();
|
||||
let schema = json_schema!({
|
||||
"type": "object",
|
||||
"properties": params
|
||||
.language_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
language_settings_content_ref.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>()
|
||||
});
|
||||
replace_subschema::<LanguageToSettingsMap>(generator, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls how completions are processed for this language.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -384,7 +410,6 @@ fn default_lsp_fetch_timeout_ms() -> u64 {
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct LanguageSettingsContent {
|
||||
/// How many columns a tab should occupy.
|
||||
///
|
||||
@@ -652,41 +677,26 @@ pub enum FormatOnSave {
|
||||
}
|
||||
|
||||
impl JsonSchema for FormatOnSave {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"OnSaveFormatter".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let formatter_schema = Formatter::json_schema(generator);
|
||||
schema.instance_type = Some(
|
||||
vec![
|
||||
InstanceType::Object,
|
||||
InstanceType::String,
|
||||
InstanceType::Array,
|
||||
|
||||
json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": formatter_schema
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["on", "off", "language_server"]
|
||||
},
|
||||
formatter_schema
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let valid_raw_values = SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
Value::String("on".into()),
|
||||
Value::String("off".into()),
|
||||
Value::String("prettier".into()),
|
||||
Value::String("language_server".into()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
let mut nested_values = SchemaObject::default();
|
||||
|
||||
nested_values.array().items = Some(formatter_schema.clone().into());
|
||||
|
||||
schema.subschemas().any_of = Some(vec![
|
||||
nested_values.into(),
|
||||
valid_raw_values.into(),
|
||||
formatter_schema,
|
||||
]);
|
||||
schema.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,8 +735,8 @@ impl<'de> Deserialize<'de> for FormatOnSave {
|
||||
} else if v == "off" {
|
||||
Ok(Self::Value::Off)
|
||||
} else if v == "language_server" {
|
||||
Ok(Self::Value::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into(),
|
||||
Ok(Self::Value::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)))
|
||||
} else {
|
||||
let ret: Result<FormatterList, _> =
|
||||
@@ -787,41 +797,26 @@ pub enum SelectedFormatter {
|
||||
}
|
||||
|
||||
impl JsonSchema for SelectedFormatter {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Formatter".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let formatter_schema = Formatter::json_schema(generator);
|
||||
schema.instance_type = Some(
|
||||
vec![
|
||||
InstanceType::Object,
|
||||
InstanceType::String,
|
||||
InstanceType::Array,
|
||||
|
||||
json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": formatter_schema
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["auto", "language_server"]
|
||||
},
|
||||
formatter_schema
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let valid_raw_values = SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
Value::String("auto".into()),
|
||||
Value::String("prettier".into()),
|
||||
Value::String("language_server".into()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut nested_values = SchemaObject::default();
|
||||
|
||||
nested_values.array().items = Some(formatter_schema.clone().into());
|
||||
|
||||
schema.subschemas().any_of = Some(vec![
|
||||
nested_values.into(),
|
||||
valid_raw_values.into(),
|
||||
formatter_schema,
|
||||
]);
|
||||
schema.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,6 +831,7 @@ impl Serialize for SelectedFormatter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
@@ -856,8 +852,8 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
if v == "auto" {
|
||||
Ok(Self::Value::Auto)
|
||||
} else if v == "language_server" {
|
||||
Ok(Self::Value::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into(),
|
||||
Ok(Self::Value::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
)))
|
||||
} else {
|
||||
let ret: Result<FormatterList, _> =
|
||||
@@ -885,16 +881,20 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
deserializer.deserialize_any(FormatDeserializer)
|
||||
}
|
||||
}
|
||||
/// Controls which formatter should be used when formatting code.
|
||||
|
||||
/// Controls which formatters should be used when formatting code.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case", transparent)]
|
||||
pub struct FormatterList(pub SingleOrVec<Formatter>);
|
||||
#[serde(untagged)]
|
||||
pub enum FormatterList {
|
||||
Single(Formatter),
|
||||
Vec(Vec<Formatter>),
|
||||
}
|
||||
|
||||
impl AsRef<[Formatter]> for FormatterList {
|
||||
fn as_ref(&self) -> &[Formatter] {
|
||||
match &self.0 {
|
||||
SingleOrVec::Single(single) => slice::from_ref(single),
|
||||
SingleOrVec::Vec(v) => v,
|
||||
match &self {
|
||||
Self::Single(single) => slice::from_ref(single),
|
||||
Self::Vec(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1209,7 +1209,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
|
||||
|
||||
let mut languages = HashMap::default();
|
||||
for (language_name, settings) in &default_value.languages {
|
||||
for (language_name, settings) in &default_value.languages.0 {
|
||||
let mut language_settings = defaults.clone();
|
||||
merge_settings(&mut language_settings, settings);
|
||||
languages.insert(language_name.clone(), language_settings);
|
||||
@@ -1310,7 +1310,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
}
|
||||
|
||||
// A user's language-specific settings override default language-specific settings.
|
||||
for (language_name, user_language_settings) in &user_settings.languages {
|
||||
for (language_name, user_language_settings) in &user_settings.languages.0 {
|
||||
merge_settings(
|
||||
languages
|
||||
.entry(language_name.clone())
|
||||
@@ -1366,51 +1366,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
})
|
||||
}
|
||||
|
||||
fn json_schema(
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
params: &settings::SettingsJsonSchemaParams,
|
||||
_: &App,
|
||||
) -> schemars::schema::RootSchema {
|
||||
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
|
||||
|
||||
// Create a schema for a 'languages overrides' object, associating editor
|
||||
// settings with specific languages.
|
||||
assert!(
|
||||
root_schema
|
||||
.definitions
|
||||
.contains_key("LanguageSettingsContent")
|
||||
);
|
||||
|
||||
let languages_object_schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties: params
|
||||
.language_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
root_schema
|
||||
.definitions
|
||||
.extend([("Languages".into(), languages_object_schema.into())]);
|
||||
|
||||
add_references_to_properties(
|
||||
&mut root_schema,
|
||||
&[("languages", "#/definitions/Languages")],
|
||||
);
|
||||
|
||||
root_schema
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
let d = &mut current.defaults;
|
||||
if let Some(size) = vscode
|
||||
@@ -1674,29 +1629,26 @@ mod tests {
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into()
|
||||
Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None }
|
||||
)))
|
||||
);
|
||||
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into()
|
||||
)))
|
||||
Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None }
|
||||
])))
|
||||
);
|
||||
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::Prettier
|
||||
]
|
||||
.into()
|
||||
)))
|
||||
Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::Prettier
|
||||
])))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,18 @@ mod telemetry;
|
||||
pub mod fake_provider;
|
||||
|
||||
use anthropic::{AnthropicError, parse_prompt_too_long};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::Client;
|
||||
use futures::FutureExt;
|
||||
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
use http_client::http;
|
||||
use http_client::{StatusCode, http};
|
||||
use icons::IconName;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use std::ops::{Add, Sub};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fmt, io};
|
||||
@@ -34,11 +35,22 @@ pub use crate::request::*;
|
||||
pub use crate::role::*;
|
||||
pub use crate::telemetry::*;
|
||||
|
||||
pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev";
|
||||
pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId =
|
||||
LanguageModelProviderId::new("anthropic");
|
||||
pub const ANTHROPIC_PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("Anthropic");
|
||||
|
||||
/// If we get a rate limit error that doesn't tell us when we can retry,
|
||||
/// default to waiting this long before retrying.
|
||||
const DEFAULT_RATE_LIMIT_RETRY_AFTER: Duration = Duration::from_secs(4);
|
||||
pub const GOOGLE_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("google");
|
||||
pub const GOOGLE_PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("Google AI");
|
||||
|
||||
pub const OPEN_AI_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openai");
|
||||
pub const OPEN_AI_PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("OpenAI");
|
||||
|
||||
pub const ZED_CLOUD_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("zed.dev");
|
||||
pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("Zed");
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut App) {
|
||||
init_settings(cx);
|
||||
@@ -71,6 +83,12 @@ pub enum LanguageModelCompletionEvent {
|
||||
data: String,
|
||||
},
|
||||
ToolUse(LanguageModelToolUse),
|
||||
ToolUseJsonParseError {
|
||||
id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
raw_input: Arc<str>,
|
||||
json_parse_error: String,
|
||||
},
|
||||
StartMessage {
|
||||
message_id: String,
|
||||
},
|
||||
@@ -79,61 +97,179 @@ pub enum LanguageModelCompletionEvent {
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LanguageModelCompletionError {
|
||||
#[error("rate limit exceeded, retry after {retry_after:?}")]
|
||||
RateLimitExceeded { retry_after: Duration },
|
||||
#[error("received bad input JSON")]
|
||||
BadInputJson {
|
||||
id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
raw_input: Arc<str>,
|
||||
json_parse_error: String,
|
||||
},
|
||||
#[error("language model provider's API is overloaded")]
|
||||
Overloaded,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
#[error("invalid request format to language model provider's API")]
|
||||
BadRequestFormat,
|
||||
#[error("authentication error with language model provider's API")]
|
||||
AuthenticationError,
|
||||
#[error("permission error with language model provider's API")]
|
||||
PermissionError,
|
||||
#[error("language model provider API endpoint not found")]
|
||||
ApiEndpointNotFound,
|
||||
#[error("prompt too large for context window")]
|
||||
PromptTooLarge { tokens: Option<u64> },
|
||||
#[error("internal server error in language model provider's API")]
|
||||
ApiInternalServerError,
|
||||
#[error("I/O error reading response from language model provider's API: {0:?}")]
|
||||
ApiReadResponseError(io::Error),
|
||||
#[error("HTTP response error from language model provider's API: status {status} - {body:?}")]
|
||||
HttpResponseError { status: u16, body: String },
|
||||
#[error("error serializing request to language model provider API: {0}")]
|
||||
SerializeRequest(serde_json::Error),
|
||||
#[error("error building request body to language model provider API: {0}")]
|
||||
BuildRequestBody(http::Error),
|
||||
#[error("error sending HTTP request to language model provider API: {0}")]
|
||||
HttpSend(anyhow::Error),
|
||||
#[error("error deserializing language model provider API response: {0}")]
|
||||
DeserializeResponse(serde_json::Error),
|
||||
#[error("unexpected language model provider API response format: {0}")]
|
||||
UnknownResponseFormat(String),
|
||||
#[error("missing {provider} API key")]
|
||||
NoApiKey { provider: LanguageModelProviderName },
|
||||
#[error("{provider}'s API rate limit exceeded")]
|
||||
RateLimitExceeded {
|
||||
provider: LanguageModelProviderName,
|
||||
retry_after: Option<Duration>,
|
||||
},
|
||||
#[error("{provider}'s API servers are overloaded right now")]
|
||||
ServerOverloaded {
|
||||
provider: LanguageModelProviderName,
|
||||
retry_after: Option<Duration>,
|
||||
},
|
||||
#[error("{provider}'s API server reported an internal server error: {message}")]
|
||||
ApiInternalServerError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("HTTP response error from {provider}'s API: status {status_code} - {message:?}")]
|
||||
HttpResponseError {
|
||||
provider: LanguageModelProviderName,
|
||||
status_code: StatusCode,
|
||||
message: String,
|
||||
},
|
||||
|
||||
// Client errors
|
||||
#[error("invalid request format to {provider}'s API: {message}")]
|
||||
BadRequestFormat {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("authentication error with {provider}'s API: {message}")]
|
||||
AuthenticationError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("permission error with {provider}'s API: {message}")]
|
||||
PermissionError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("language model provider API endpoint not found")]
|
||||
ApiEndpointNotFound { provider: LanguageModelProviderName },
|
||||
#[error("I/O error reading response from {provider}'s API")]
|
||||
ApiReadResponseError {
|
||||
provider: LanguageModelProviderName,
|
||||
#[source]
|
||||
error: io::Error,
|
||||
},
|
||||
#[error("error serializing request to {provider} API")]
|
||||
SerializeRequest {
|
||||
provider: LanguageModelProviderName,
|
||||
#[source]
|
||||
error: serde_json::Error,
|
||||
},
|
||||
#[error("error building request body to {provider} API")]
|
||||
BuildRequestBody {
|
||||
provider: LanguageModelProviderName,
|
||||
#[source]
|
||||
error: http::Error,
|
||||
},
|
||||
#[error("error sending HTTP request to {provider} API")]
|
||||
HttpSend {
|
||||
provider: LanguageModelProviderName,
|
||||
#[source]
|
||||
error: anyhow::Error,
|
||||
},
|
||||
#[error("error deserializing {provider} API response")]
|
||||
DeserializeResponse {
|
||||
provider: LanguageModelProviderName,
|
||||
#[source]
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
// TODO: Ideally this would be removed in favor of having a comprehensive list of errors.
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionError {
|
||||
pub fn from_cloud_failure(
|
||||
upstream_provider: LanguageModelProviderName,
|
||||
code: String,
|
||||
message: String,
|
||||
retry_after: Option<Duration>,
|
||||
) -> Self {
|
||||
if let Some(tokens) = parse_prompt_too_long(&message) {
|
||||
// TODO: currently Anthropic PAYLOAD_TOO_LARGE response may cause INTERNAL_SERVER_ERROR
|
||||
// to be reported. This is a temporary workaround to handle this in the case where the
|
||||
// token limit has been exceeded.
|
||||
Self::PromptTooLarge {
|
||||
tokens: Some(tokens),
|
||||
}
|
||||
} else if let Some(status_code) = code
|
||||
.strip_prefix("upstream_http_")
|
||||
.and_then(|code| StatusCode::from_str(code).ok())
|
||||
{
|
||||
Self::from_http_status(upstream_provider, status_code, message, retry_after)
|
||||
} else if let Some(status_code) = code
|
||||
.strip_prefix("http_")
|
||||
.and_then(|code| StatusCode::from_str(code).ok())
|
||||
{
|
||||
Self::from_http_status(ZED_CLOUD_PROVIDER_NAME, status_code, message, retry_after)
|
||||
} else {
|
||||
anyhow!("completion request failed, code: {code}, message: {message}").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_http_status(
|
||||
provider: LanguageModelProviderName,
|
||||
status_code: StatusCode,
|
||||
message: String,
|
||||
retry_after: Option<Duration>,
|
||||
) -> Self {
|
||||
match status_code {
|
||||
StatusCode::BAD_REQUEST => Self::BadRequestFormat { provider, message },
|
||||
StatusCode::UNAUTHORIZED => Self::AuthenticationError { provider, message },
|
||||
StatusCode::FORBIDDEN => Self::PermissionError { provider, message },
|
||||
StatusCode::NOT_FOUND => Self::ApiEndpointNotFound { provider },
|
||||
StatusCode::PAYLOAD_TOO_LARGE => Self::PromptTooLarge {
|
||||
tokens: parse_prompt_too_long(&message),
|
||||
},
|
||||
StatusCode::TOO_MANY_REQUESTS => Self::RateLimitExceeded {
|
||||
provider,
|
||||
retry_after,
|
||||
},
|
||||
StatusCode::INTERNAL_SERVER_ERROR => Self::ApiInternalServerError { provider, message },
|
||||
StatusCode::SERVICE_UNAVAILABLE => Self::ServerOverloaded {
|
||||
provider,
|
||||
retry_after,
|
||||
},
|
||||
_ if status_code.as_u16() == 529 => Self::ServerOverloaded {
|
||||
provider,
|
||||
retry_after,
|
||||
},
|
||||
_ => Self::HttpResponseError {
|
||||
provider,
|
||||
status_code,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnthropicError> for LanguageModelCompletionError {
|
||||
fn from(error: AnthropicError) -> Self {
|
||||
let provider = ANTHROPIC_PROVIDER_NAME;
|
||||
match error {
|
||||
AnthropicError::SerializeRequest(error) => Self::SerializeRequest(error),
|
||||
AnthropicError::BuildRequestBody(error) => Self::BuildRequestBody(error),
|
||||
AnthropicError::HttpSend(error) => Self::HttpSend(error),
|
||||
AnthropicError::DeserializeResponse(error) => Self::DeserializeResponse(error),
|
||||
AnthropicError::ReadResponse(error) => Self::ApiReadResponseError(error),
|
||||
AnthropicError::HttpResponseError { status, body } => {
|
||||
Self::HttpResponseError { status, body }
|
||||
AnthropicError::SerializeRequest(error) => Self::SerializeRequest { provider, error },
|
||||
AnthropicError::BuildRequestBody(error) => Self::BuildRequestBody { provider, error },
|
||||
AnthropicError::HttpSend(error) => Self::HttpSend { provider, error },
|
||||
AnthropicError::DeserializeResponse(error) => {
|
||||
Self::DeserializeResponse { provider, error }
|
||||
}
|
||||
AnthropicError::RateLimit { retry_after } => Self::RateLimitExceeded { retry_after },
|
||||
AnthropicError::ReadResponse(error) => Self::ApiReadResponseError { provider, error },
|
||||
AnthropicError::HttpResponseError {
|
||||
status_code,
|
||||
message,
|
||||
} => Self::HttpResponseError {
|
||||
provider,
|
||||
status_code,
|
||||
message,
|
||||
},
|
||||
AnthropicError::RateLimit { retry_after } => Self::RateLimitExceeded {
|
||||
provider,
|
||||
retry_after: Some(retry_after),
|
||||
},
|
||||
AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: retry_after,
|
||||
},
|
||||
AnthropicError::ApiError(api_error) => api_error.into(),
|
||||
AnthropicError::UnexpectedResponseFormat(error) => Self::UnknownResponseFormat(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,23 +277,39 @@ impl From<AnthropicError> for LanguageModelCompletionError {
|
||||
impl From<anthropic::ApiError> for LanguageModelCompletionError {
|
||||
fn from(error: anthropic::ApiError) -> Self {
|
||||
use anthropic::ApiErrorCode::*;
|
||||
|
||||
let provider = ANTHROPIC_PROVIDER_NAME;
|
||||
match error.code() {
|
||||
Some(code) => match code {
|
||||
InvalidRequestError => LanguageModelCompletionError::BadRequestFormat,
|
||||
AuthenticationError => LanguageModelCompletionError::AuthenticationError,
|
||||
PermissionError => LanguageModelCompletionError::PermissionError,
|
||||
NotFoundError => LanguageModelCompletionError::ApiEndpointNotFound,
|
||||
RequestTooLarge => LanguageModelCompletionError::PromptTooLarge {
|
||||
InvalidRequestError => Self::BadRequestFormat {
|
||||
provider,
|
||||
message: error.message,
|
||||
},
|
||||
AuthenticationError => Self::AuthenticationError {
|
||||
provider,
|
||||
message: error.message,
|
||||
},
|
||||
PermissionError => Self::PermissionError {
|
||||
provider,
|
||||
message: error.message,
|
||||
},
|
||||
NotFoundError => Self::ApiEndpointNotFound { provider },
|
||||
RequestTooLarge => Self::PromptTooLarge {
|
||||
tokens: parse_prompt_too_long(&error.message),
|
||||
},
|
||||
RateLimitError => LanguageModelCompletionError::RateLimitExceeded {
|
||||
retry_after: DEFAULT_RATE_LIMIT_RETRY_AFTER,
|
||||
RateLimitError => Self::RateLimitExceeded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
},
|
||||
ApiError => Self::ApiInternalServerError {
|
||||
provider,
|
||||
message: error.message,
|
||||
},
|
||||
OverloadedError => Self::ServerOverloaded {
|
||||
provider,
|
||||
retry_after: None,
|
||||
},
|
||||
ApiError => LanguageModelCompletionError::ApiInternalServerError,
|
||||
OverloadedError => LanguageModelCompletionError::Overloaded,
|
||||
},
|
||||
None => LanguageModelCompletionError::Other(error.into()),
|
||||
None => Self::Other(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +430,13 @@ pub trait LanguageModel: Send + Sync {
|
||||
fn name(&self) -> LanguageModelName;
|
||||
fn provider_id(&self) -> LanguageModelProviderId;
|
||||
fn provider_name(&self) -> LanguageModelProviderName;
|
||||
fn upstream_provider_id(&self) -> LanguageModelProviderId {
|
||||
self.provider_id()
|
||||
}
|
||||
fn upstream_provider_name(&self) -> LanguageModelProviderName {
|
||||
self.provider_name()
|
||||
}
|
||||
|
||||
fn telemetry_id(&self) -> String;
|
||||
|
||||
fn api_key(&self, _cx: &App) -> Option<String> {
|
||||
@@ -365,6 +524,9 @@ pub trait LanguageModel: Send + Sync {
|
||||
Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => None,
|
||||
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
..
|
||||
}) => None,
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||
*last_token_usage.lock() = token_usage;
|
||||
None
|
||||
@@ -395,39 +557,6 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LanguageModelKnownError {
|
||||
#[error("Context window limit exceeded ({tokens})")]
|
||||
ContextWindowLimitExceeded { tokens: u64 },
|
||||
#[error("Language model provider's API is currently overloaded")]
|
||||
Overloaded,
|
||||
#[error("Language model provider's API encountered an internal server error")]
|
||||
ApiInternalServerError,
|
||||
#[error("I/O error while reading response from language model provider's API: {0:?}")]
|
||||
ReadResponseError(io::Error),
|
||||
#[error("Error deserializing response from language model provider's API: {0:?}")]
|
||||
DeserializeResponse(serde_json::Error),
|
||||
#[error("Language model provider's API returned a response in an unknown format")]
|
||||
UnknownResponseFormat(String),
|
||||
#[error("Rate limit exceeded for language model provider's API; retry in {retry_after:?}")]
|
||||
RateLimitExceeded { retry_after: Duration },
|
||||
}
|
||||
|
||||
impl LanguageModelKnownError {
|
||||
/// Attempts to map an HTTP response status code to a known error type.
|
||||
/// Returns None if the status code doesn't map to a specific known error.
|
||||
pub fn from_http_response(status: u16, _body: &str) -> Option<Self> {
|
||||
match status {
|
||||
429 => Some(Self::RateLimitExceeded {
|
||||
retry_after: DEFAULT_RATE_LIMIT_RETRY_AFTER,
|
||||
}),
|
||||
503 => Some(Self::Overloaded),
|
||||
500..=599 => Some(Self::ApiInternalServerError),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
|
||||
fn name() -> String;
|
||||
fn description() -> String;
|
||||
@@ -473,7 +602,7 @@ pub trait LanguageModelProvider: 'static {
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum LanguageModelProviderTosView {
|
||||
/// When there are some past interactions in the Agent Panel.
|
||||
ThreadtEmptyState,
|
||||
ThreadEmptyState,
|
||||
/// When there are no past interactions in the Agent Panel.
|
||||
ThreadFreshStart,
|
||||
PromptEditorPopup,
|
||||
@@ -509,12 +638,30 @@ pub struct LanguageModelProviderId(pub SharedString);
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
|
||||
pub struct LanguageModelProviderName(pub SharedString);
|
||||
|
||||
impl LanguageModelProviderId {
|
||||
pub const fn new(id: &'static str) -> Self {
|
||||
Self(SharedString::new_static(id))
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelProviderName {
|
||||
pub const fn new(id: &'static str) -> Self {
|
||||
Self(SharedString::new_static(id))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LanguageModelProviderId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LanguageModelProviderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for LanguageModelId {
|
||||
fn from(value: String) -> Self {
|
||||
Self(SharedString::from(value))
|
||||
|
||||
@@ -98,7 +98,7 @@ impl ConfiguredModel {
|
||||
}
|
||||
|
||||
pub fn is_provided_by_zed(&self) -> bool {
|
||||
self.provider.id().0 == crate::ZED_CLOUD_PROVIDER_ID
|
||||
self.provider.id() == crate::ZED_CLOUD_PROVIDER_ID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::ANTHROPIC_PROVIDER_ID;
|
||||
use anthropic::ANTHROPIC_API_URL;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -8,8 +9,6 @@ use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use util::ResultExt;
|
||||
|
||||
pub const ANTHROPIC_PROVIDER_ID: &str = "anthropic";
|
||||
|
||||
pub fn report_assistant_event(
|
||||
event: AssistantEventData,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
@@ -19,7 +18,7 @@ pub fn report_assistant_event(
|
||||
) {
|
||||
if let Some(telemetry) = telemetry.as_ref() {
|
||||
telemetry.report_assistant_event(event.clone());
|
||||
if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID {
|
||||
if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID.0 {
|
||||
if let Some(api_key) = model_api_key {
|
||||
executor
|
||||
.spawn(async move {
|
||||
|
||||
@@ -20,8 +20,10 @@ aws-credential-types = { workspace = true, features = [
|
||||
] }
|
||||
aws_http_client.workspace = true
|
||||
bedrock.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
copilot.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
|
||||
@@ -33,8 +33,8 @@ use theme::ThemeSettings;
|
||||
use ui::{Icon, IconName, List, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
const PROVIDER_ID: &str = language_model::ANTHROPIC_PROVIDER_ID;
|
||||
const PROVIDER_NAME: &str = "Anthropic";
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ANTHROPIC_PROVIDER_NAME;
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct AnthropicSettings {
|
||||
@@ -218,11 +218,11 @@ impl LanguageModelProviderState for AnthropicLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -403,7 +403,11 @@ impl AnthropicModel {
|
||||
};
|
||||
|
||||
async move {
|
||||
let api_key = api_key.context("Missing Anthropic API Key")?;
|
||||
let Some(api_key) = api_key else {
|
||||
return Err(LanguageModelCompletionError::NoApiKey {
|
||||
provider: PROVIDER_NAME,
|
||||
});
|
||||
};
|
||||
let request =
|
||||
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
|
||||
request.await.map_err(Into::into)
|
||||
@@ -422,11 +426,11 @@ impl LanguageModel for AnthropicModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
@@ -528,6 +532,11 @@ pub fn into_anthropic(
|
||||
.into_iter()
|
||||
.filter_map(|content| match content {
|
||||
MessageContent::Text(text) => {
|
||||
let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) {
|
||||
text.trim_end().to_string()
|
||||
} else {
|
||||
text
|
||||
};
|
||||
if !text.is_empty() {
|
||||
Some(anthropic::RequestContent::Text {
|
||||
text,
|
||||
@@ -801,12 +810,14 @@ impl AnthropicEventMapper {
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
},
|
||||
)),
|
||||
Err(json_parse_err) => Err(LanguageModelCompletionError::BadInputJson {
|
||||
id: tool_use.id.into(),
|
||||
tool_name: tool_use.name.into(),
|
||||
raw_input: input_json.into(),
|
||||
json_parse_error: json_parse_err.to_string(),
|
||||
}),
|
||||
Err(json_parse_err) => {
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id: tool_use.id.into(),
|
||||
tool_name: tool_use.name.into(),
|
||||
raw_input: input_json.into(),
|
||||
json_parse_error: json_parse_err.to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
vec![event_result]
|
||||
|
||||
@@ -46,14 +46,13 @@ use settings::{Settings, SettingsStore};
|
||||
use smol::lock::OnceCell;
|
||||
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
|
||||
use theme::ThemeSettings;
|
||||
use tokio::runtime::Handle;
|
||||
use ui::{Icon, IconName, List, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
|
||||
const PROVIDER_ID: &str = "amazon-bedrock";
|
||||
const PROVIDER_NAME: &str = "Amazon Bedrock";
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock");
|
||||
|
||||
#[derive(Default, Clone, Deserialize, Serialize, PartialEq, Debug)]
|
||||
pub struct BedrockCredentials {
|
||||
@@ -285,11 +284,11 @@ impl BedrockLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for BedrockLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -460,22 +459,22 @@ impl BedrockModel {
|
||||
&self,
|
||||
request: bedrock::Request,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<
|
||||
BoxFuture<'static, BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
|
||||
> {
|
||||
let runtime_client = self
|
||||
.get_or_init_client(cx)
|
||||
let Ok(runtime_client) = self
|
||||
.get_or_init_client(&cx)
|
||||
.cloned()
|
||||
.context("Bedrock client not initialized")?;
|
||||
let owned_handle = self.handler.clone();
|
||||
.context("Bedrock client not initialized")
|
||||
else {
|
||||
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
|
||||
};
|
||||
|
||||
Ok(async move {
|
||||
let request = bedrock::stream_completion(runtime_client, request, owned_handle);
|
||||
request.await.unwrap_or_else(|e| {
|
||||
futures::stream::once(async move { Err(BedrockError::ClientError(e)) }).boxed()
|
||||
})
|
||||
match Tokio::spawn(cx, bedrock::stream_completion(runtime_client, request)) {
|
||||
Ok(res) => async { res.await.map_err(|err| anyhow!(err))? }.boxed(),
|
||||
Err(err) => futures::future::ready(Err(anyhow!(err))).boxed(),
|
||||
}
|
||||
.boxed())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,11 +488,11 @@ impl LanguageModel for BedrockModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
@@ -570,12 +569,10 @@ impl LanguageModel for BedrockModel {
|
||||
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
|
||||
};
|
||||
|
||||
let owned_handle = self.handler.clone();
|
||||
|
||||
let request = self.stream_completion(request, cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = request.map_err(|err| anyhow!(err))?.await;
|
||||
let events = map_to_language_model_completion_events(response, owned_handle);
|
||||
let response = request.await.map_err(|err| anyhow!(err))?;
|
||||
let events = map_to_language_model_completion_events(response);
|
||||
|
||||
if deny_tool_calls {
|
||||
Ok(deny_tool_use_events(events).boxed())
|
||||
@@ -879,7 +876,6 @@ pub fn get_bedrock_tokens(
|
||||
|
||||
pub fn map_to_language_model_completion_events(
|
||||
events: Pin<Box<dyn Send + Stream<Item = Result<BedrockStreamingResponse, BedrockError>>>>,
|
||||
handle: Handle,
|
||||
) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
||||
struct RawToolUse {
|
||||
id: String,
|
||||
@@ -892,198 +888,123 @@ pub fn map_to_language_model_completion_events(
|
||||
tool_uses_by_index: HashMap<i32, RawToolUse>,
|
||||
}
|
||||
|
||||
futures::stream::unfold(
|
||||
State {
|
||||
events,
|
||||
tool_uses_by_index: HashMap::default(),
|
||||
},
|
||||
move |mut state: State| {
|
||||
let inner_handle = handle.clone();
|
||||
async move {
|
||||
inner_handle
|
||||
.spawn(async {
|
||||
while let Some(event) = state.events.next().await {
|
||||
match event {
|
||||
Ok(event) => match event {
|
||||
ConverseStreamOutput::ContentBlockDelta(cb_delta) => {
|
||||
match cb_delta.delta {
|
||||
Some(ContentBlockDelta::Text(text_out)) => {
|
||||
let completion_event =
|
||||
LanguageModelCompletionEvent::Text(text_out);
|
||||
return Some((Some(Ok(completion_event)), state));
|
||||
}
|
||||
let initial_state = State {
|
||||
events,
|
||||
tool_uses_by_index: HashMap::default(),
|
||||
};
|
||||
|
||||
Some(ContentBlockDelta::ToolUse(text_out)) => {
|
||||
if let Some(tool_use) = state
|
||||
.tool_uses_by_index
|
||||
.get_mut(&cb_delta.content_block_index)
|
||||
{
|
||||
tool_use.input_json.push_str(text_out.input());
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContentBlockDelta::ReasoningContent(thinking)) => {
|
||||
match thinking {
|
||||
ReasoningContentBlockDelta::RedactedContent(
|
||||
redacted,
|
||||
) => {
|
||||
let thinking_event =
|
||||
LanguageModelCompletionEvent::Thinking {
|
||||
text: String::from_utf8(
|
||||
redacted.into_inner(),
|
||||
)
|
||||
.unwrap_or("REDACTED".to_string()),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
return Some((
|
||||
Some(Ok(thinking_event)),
|
||||
state,
|
||||
));
|
||||
}
|
||||
ReasoningContentBlockDelta::Signature(
|
||||
signature,
|
||||
) => {
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::Thinking {
|
||||
text: "".to_string(),
|
||||
signature: Some(signature)
|
||||
})),
|
||||
state,
|
||||
));
|
||||
}
|
||||
ReasoningContentBlockDelta::Text(thoughts) => {
|
||||
let thinking_event =
|
||||
LanguageModelCompletionEvent::Thinking {
|
||||
text: thoughts.to_string(),
|
||||
signature: None
|
||||
};
|
||||
|
||||
return Some((
|
||||
Some(Ok(thinking_event)),
|
||||
state,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ConverseStreamOutput::ContentBlockStart(cb_start) => {
|
||||
if let Some(ContentBlockStart::ToolUse(text_out)) =
|
||||
cb_start.start
|
||||
{
|
||||
let tool_use = RawToolUse {
|
||||
id: text_out.tool_use_id,
|
||||
name: text_out.name,
|
||||
input_json: String::new(),
|
||||
};
|
||||
|
||||
state
|
||||
.tool_uses_by_index
|
||||
.insert(cb_start.content_block_index, tool_use);
|
||||
}
|
||||
}
|
||||
ConverseStreamOutput::ContentBlockStop(cb_stop) => {
|
||||
if let Some(tool_use) = state
|
||||
.tool_uses_by_index
|
||||
.remove(&cb_stop.content_block_index)
|
||||
{
|
||||
let tool_use_event = LanguageModelToolUse {
|
||||
id: tool_use.id.into(),
|
||||
name: tool_use.name.into(),
|
||||
is_input_complete: true,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
input: if tool_use.input_json.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
serde_json::Value::from_str(
|
||||
&tool_use.input_json,
|
||||
)
|
||||
.map_err(|err| anyhow!(err))
|
||||
.unwrap()
|
||||
},
|
||||
};
|
||||
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
tool_use_event,
|
||||
))),
|
||||
state,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ConverseStreamOutput::Metadata(cb_meta) => {
|
||||
if let Some(metadata) = cb_meta.usage {
|
||||
let completion_event =
|
||||
LanguageModelCompletionEvent::UsageUpdate(
|
||||
TokenUsage {
|
||||
input_tokens: metadata.input_tokens as u64,
|
||||
output_tokens: metadata.output_tokens as u64,
|
||||
cache_creation_input_tokens:
|
||||
metadata.cache_write_input_tokens.unwrap_or_default() as u64,
|
||||
cache_read_input_tokens:
|
||||
metadata.cache_read_input_tokens.unwrap_or_default() as u64,
|
||||
},
|
||||
);
|
||||
return Some((Some(Ok(completion_event)), state));
|
||||
}
|
||||
}
|
||||
ConverseStreamOutput::MessageStop(message_stop) => {
|
||||
let reason = match message_stop.stop_reason {
|
||||
StopReason::ContentFiltered => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
)
|
||||
}
|
||||
StopReason::EndTurn => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
)
|
||||
}
|
||||
StopReason::GuardrailIntervened => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
)
|
||||
}
|
||||
StopReason::MaxTokens => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
)
|
||||
}
|
||||
StopReason::StopSequence => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
)
|
||||
}
|
||||
StopReason::ToolUse => {
|
||||
LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::ToolUse,
|
||||
)
|
||||
}
|
||||
_ => LanguageModelCompletionEvent::Stop(
|
||||
language_model::StopReason::EndTurn,
|
||||
),
|
||||
};
|
||||
return Some((Some(Ok(reason)), state));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Err(err) => return Some((Some(Err(anyhow!(err).into())), state)),
|
||||
futures::stream::unfold(initial_state, |mut state| async move {
|
||||
match state.events.next().await {
|
||||
Some(event_result) => match event_result {
|
||||
Ok(event) => {
|
||||
let result = match event {
|
||||
ConverseStreamOutput::ContentBlockDelta(cb_delta) => match cb_delta.delta {
|
||||
Some(ContentBlockDelta::Text(text)) => {
|
||||
Some(Ok(LanguageModelCompletionEvent::Text(text)))
|
||||
}
|
||||
Some(ContentBlockDelta::ToolUse(tool_output)) => {
|
||||
if let Some(tool_use) = state
|
||||
.tool_uses_by_index
|
||||
.get_mut(&cb_delta.content_block_index)
|
||||
{
|
||||
tool_use.input_json.push_str(tool_output.input());
|
||||
}
|
||||
None
|
||||
}
|
||||
Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking {
|
||||
ReasoningContentBlockDelta::Text(thoughts) => {
|
||||
Some(Ok(LanguageModelCompletionEvent::Thinking {
|
||||
text: thoughts.clone(),
|
||||
signature: None,
|
||||
}))
|
||||
}
|
||||
ReasoningContentBlockDelta::Signature(sig) => {
|
||||
Some(Ok(LanguageModelCompletionEvent::Thinking {
|
||||
text: "".into(),
|
||||
signature: Some(sig),
|
||||
}))
|
||||
}
|
||||
ReasoningContentBlockDelta::RedactedContent(redacted) => {
|
||||
let content = String::from_utf8(redacted.into_inner())
|
||||
.unwrap_or("REDACTED".to_string());
|
||||
Some(Ok(LanguageModelCompletionEvent::Thinking {
|
||||
text: content,
|
||||
signature: None,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
ConverseStreamOutput::ContentBlockStart(cb_start) => {
|
||||
if let Some(ContentBlockStart::ToolUse(tool_start)) = cb_start.start {
|
||||
state.tool_uses_by_index.insert(
|
||||
cb_start.content_block_index,
|
||||
RawToolUse {
|
||||
id: tool_start.tool_use_id,
|
||||
name: tool_start.name,
|
||||
input_json: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
None
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
}
|
||||
},
|
||||
)
|
||||
.filter_map(|event| async move { event })
|
||||
ConverseStreamOutput::ContentBlockStop(cb_stop) => state
|
||||
.tool_uses_by_index
|
||||
.remove(&cb_stop.content_block_index)
|
||||
.map(|tool_use| {
|
||||
let input = if tool_use.input_json.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
serde_json::Value::from_str(&tool_use.input_json)
|
||||
.unwrap_or(Value::Null)
|
||||
};
|
||||
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: tool_use.id.into(),
|
||||
name: tool_use.name.into(),
|
||||
is_input_complete: true,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
input,
|
||||
},
|
||||
))
|
||||
}),
|
||||
ConverseStreamOutput::Metadata(cb_meta) => cb_meta.usage.map(|metadata| {
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
|
||||
input_tokens: metadata.input_tokens as u64,
|
||||
output_tokens: metadata.output_tokens as u64,
|
||||
cache_creation_input_tokens: metadata
|
||||
.cache_write_input_tokens
|
||||
.unwrap_or_default()
|
||||
as u64,
|
||||
cache_read_input_tokens: metadata
|
||||
.cache_read_input_tokens
|
||||
.unwrap_or_default()
|
||||
as u64,
|
||||
}))
|
||||
}),
|
||||
ConverseStreamOutput::MessageStop(message_stop) => {
|
||||
let stop_reason = match message_stop.stop_reason {
|
||||
StopReason::ToolUse => language_model::StopReason::ToolUse,
|
||||
_ => language_model::StopReason::EndTurn,
|
||||
};
|
||||
Some(Ok(LanguageModelCompletionEvent::Stop(stop_reason)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some((result, state))
|
||||
}
|
||||
Err(err) => Some((
|
||||
Some(Err(LanguageModelCompletionError::Other(anyhow!(err)))),
|
||||
state,
|
||||
)),
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.filter_map(|result| async move { result })
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anthropic::{AnthropicModelMode, parse_prompt_too_long};
|
||||
use anthropic::AnthropicModelMode;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
@@ -8,25 +9,21 @@ use google_ai::GoogleModelMode;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, SemanticVersion, Subscription, Task,
|
||||
};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode};
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionError, LanguageModelId, LanguageModelKnownError, LanguageModelName,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelProviderTosView, LanguageModelRequest, LanguageModelToolChoice,
|
||||
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken, PaymentRequiredError,
|
||||
RefreshLlmTokenListener,
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
|
||||
};
|
||||
use proto::Plan;
|
||||
use release_channel::AppVersion;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use settings::SettingsStore;
|
||||
use smol::Timer;
|
||||
use smol::io::{AsyncReadExt, BufReader};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr as _;
|
||||
@@ -47,7 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||
|
||||
pub const PROVIDER_NAME: &str = "Zed";
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct ZedDotDevSettings {
|
||||
@@ -120,7 +118,7 @@ pub struct State {
|
||||
llm_api_token: LlmApiToken,
|
||||
user_store: Entity<UserStore>,
|
||||
status: client::Status,
|
||||
accept_terms: Option<Task<Result<()>>>,
|
||||
accept_terms_of_service_task: Option<Task<Result<()>>>,
|
||||
models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
@@ -144,7 +142,7 @@ impl State {
|
||||
llm_api_token: LlmApiToken::default(),
|
||||
user_store,
|
||||
status,
|
||||
accept_terms: None,
|
||||
accept_terms_of_service_task: None,
|
||||
models: Vec::new(),
|
||||
default_model: None,
|
||||
default_fast_model: None,
|
||||
@@ -253,12 +251,12 @@ impl State {
|
||||
|
||||
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
self.accept_terms = Some(cx.spawn(async move |this, cx| {
|
||||
self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| {
|
||||
let _ = user_store
|
||||
.update(cx, |store, cx| store.accept_terms_of_service(cx))?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.accept_terms = None;
|
||||
this.accept_terms_of_service_task = None;
|
||||
cx.notify()
|
||||
})
|
||||
}));
|
||||
@@ -351,11 +349,11 @@ impl LanguageModelProviderState for CloudLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -397,7 +395,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
}
|
||||
|
||||
fn is_authenticated(&self, cx: &App) -> bool {
|
||||
!self.state.read(cx).is_signed_out()
|
||||
let state = self.state.read(cx);
|
||||
!state.is_signed_out() && state.has_accepted_terms_of_service(cx)
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
|
||||
@@ -405,10 +404,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
}
|
||||
|
||||
fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
|
||||
cx.new(|_| ConfigurationView {
|
||||
state: self.state.clone(),
|
||||
})
|
||||
.into()
|
||||
cx.new(|_| ConfigurationView::new(self.state.clone()))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn must_accept_terms(&self, cx: &App) -> bool {
|
||||
@@ -420,7 +417,19 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
view: LanguageModelProviderTosView,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
render_accept_terms(self.state.clone(), view, cx)
|
||||
let state = self.state.read(cx);
|
||||
if state.has_accepted_terms_of_service(cx) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
render_accept_terms(view, state.accept_terms_of_service_task.is_some(), {
|
||||
let state = self.state.clone();
|
||||
move |_window, cx| {
|
||||
state.update(cx, |state, cx| state.accept_terms_of_service(cx));
|
||||
}
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
@@ -429,18 +438,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
}
|
||||
|
||||
fn render_accept_terms(
|
||||
state: Entity<State>,
|
||||
view_kind: LanguageModelProviderTosView,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
if state.read(cx).has_accepted_terms_of_service(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let accept_terms_disabled = state.read(cx).accept_terms.is_some();
|
||||
|
||||
accept_terms_of_service_in_progress: bool,
|
||||
accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
|
||||
let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState);
|
||||
let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState);
|
||||
|
||||
let terms_button = Button::new("terms_of_service", "Terms of Service")
|
||||
.style(ButtonStyle::Subtle)
|
||||
@@ -463,18 +466,11 @@ fn render_accept_terms(
|
||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
})
|
||||
.disabled(accept_terms_disabled)
|
||||
.on_click({
|
||||
let state = state.downgrade();
|
||||
move |_, _window, cx| {
|
||||
state
|
||||
.update(cx, |state, cx| state.accept_terms_of_service(cx))
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
.disabled(accept_terms_of_service_in_progress)
|
||||
.on_click(move |_, window, cx| (accept_terms_callback)(window, cx)),
|
||||
);
|
||||
|
||||
let form = if thread_empty_state {
|
||||
if thread_empty_state {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_wrap()
|
||||
@@ -512,12 +508,10 @@ fn render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadFreshStart => {
|
||||
button_container.w_full().justify_center()
|
||||
}
|
||||
LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(),
|
||||
LanguageModelProviderTosView::ThreadEmptyState => div().w_0(),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(form.into_any())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CloudLanguageModel {
|
||||
@@ -536,8 +530,6 @@ struct PerformLlmCompletionResponse {
|
||||
}
|
||||
|
||||
impl CloudLanguageModel {
|
||||
const MAX_RETRIES: usize = 3;
|
||||
|
||||
async fn perform_llm_completion(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
@@ -547,8 +539,7 @@ impl CloudLanguageModel {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
let mut token = llm_api_token.acquire(&client).await?;
|
||||
let mut retries_remaining = Self::MAX_RETRIES;
|
||||
let mut retry_delay = Duration::from_secs(1);
|
||||
let mut refreshed_token = false;
|
||||
|
||||
loop {
|
||||
let request_builder = http_client::Request::builder()
|
||||
@@ -590,14 +581,20 @@ impl CloudLanguageModel {
|
||||
includes_status_messages,
|
||||
tool_use_limit_reached,
|
||||
});
|
||||
} else if response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
if !refreshed_token
|
||||
&& response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
retries_remaining -= 1;
|
||||
token = llm_api_token.refresh(&client).await?;
|
||||
} else if status == StatusCode::FORBIDDEN
|
||||
refreshed_token = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if status == StatusCode::FORBIDDEN
|
||||
&& response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
@@ -622,35 +619,18 @@ impl CloudLanguageModel {
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("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:
|
||||
// * 500 Internal Server Error
|
||||
// * 502 Bad Gateway
|
||||
// * 529 Service Overloaded
|
||||
|
||||
if retries_remaining == 0 {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
anyhow::bail!(
|
||||
"cloud language model completion failed after {} retries with status {status}: {body}",
|
||||
Self::MAX_RETRIES
|
||||
);
|
||||
}
|
||||
|
||||
Timer::after(retry_delay).await;
|
||||
|
||||
retries_remaining -= 1;
|
||||
retry_delay *= 2; // If it fails again, wait longer.
|
||||
} else if status == StatusCode::PAYMENT_REQUIRED {
|
||||
return Err(anyhow!(PaymentRequiredError));
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(ApiError { status, body }));
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
let headers = response.headers().clone();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(ApiError {
|
||||
status,
|
||||
body,
|
||||
headers
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,6 +640,19 @@ impl CloudLanguageModel {
|
||||
struct ApiError {
|
||||
status: StatusCode,
|
||||
body: String,
|
||||
headers: HeaderMap<HeaderValue>,
|
||||
}
|
||||
|
||||
impl From<ApiError> for LanguageModelCompletionError {
|
||||
fn from(error: ApiError) -> Self {
|
||||
let retry_after = None;
|
||||
LanguageModelCompletionError::from_http_status(
|
||||
PROVIDER_NAME,
|
||||
error.status,
|
||||
error.body,
|
||||
retry_after,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModel for CloudLanguageModel {
|
||||
@@ -672,11 +665,29 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn upstream_provider_id(&self) -> LanguageModelProviderId {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_ID,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_ID,
|
||||
Google => language_model::GOOGLE_PROVIDER_ID,
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_provider_name(&self) -> LanguageModelProviderName {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_NAME,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_NAME,
|
||||
Google => language_model::GOOGLE_PROVIDER_NAME,
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
@@ -776,6 +787,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
.body(serde_json::to_string(&request_body)?.into())?;
|
||||
let mut response = http_client.send(request).await?;
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let mut response_body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
@@ -790,7 +802,8 @@ impl LanguageModel for CloudLanguageModel {
|
||||
} else {
|
||||
Err(anyhow!(ApiError {
|
||||
status,
|
||||
body: response_body
|
||||
body: response_body,
|
||||
headers
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -855,18 +868,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err.downcast::<ApiError>() {
|
||||
Ok(api_err) => {
|
||||
if api_err.status == StatusCode::BAD_REQUEST {
|
||||
if let Some(tokens) = parse_prompt_too_long(&api_err.body) {
|
||||
return anyhow!(
|
||||
LanguageModelKnownError::ContextWindowLimitExceeded {
|
||||
tokens
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
anyhow!(api_err)
|
||||
}
|
||||
Ok(api_err) => anyhow!(LanguageModelCompletionError::from(api_err)),
|
||||
Err(err) => anyhow!(err),
|
||||
})?;
|
||||
|
||||
@@ -995,7 +997,7 @@ where
|
||||
.flat_map(move |event| {
|
||||
futures::stream::iter(match event {
|
||||
Err(error) => {
|
||||
vec![Err(LanguageModelCompletionError::Other(error))]
|
||||
vec![Err(LanguageModelCompletionError::from(error))]
|
||||
}
|
||||
Ok(CloudCompletionEvent::Status(event)) => {
|
||||
vec![Ok(LanguageModelCompletionEvent::StatusUpdate(event))]
|
||||
@@ -1054,32 +1056,24 @@ fn response_lines<T: DeserializeOwned>(
|
||||
)
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
state: gpui::Entity<State>,
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
struct ZedAiConfiguration {
|
||||
is_connected: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
||||
eligible_for_trial: bool,
|
||||
has_accepted_terms_of_service: bool,
|
||||
accept_terms_of_service_in_progress: bool,
|
||||
accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.authenticate(cx).detach_and_log_err(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
impl RenderOnce for ZedAiConfiguration {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
const ZED_PRICING_URL: &str = "https://zed.dev/pricing";
|
||||
|
||||
let is_connected = !self.state.read(cx).is_signed_out();
|
||||
let user_store = self.state.read(cx).user_store.read(cx);
|
||||
let plan = user_store.current_plan();
|
||||
let subscription_period = user_store.subscription_period();
|
||||
let eligible_for_trial = user_store.trial_started_at().is_none();
|
||||
let has_accepted_terms = self.state.read(cx).has_accepted_terms_of_service(cx);
|
||||
|
||||
let is_pro = plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = match (plan, subscription_period) {
|
||||
let is_pro = self.plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = match (self.plan, self.subscription_period) {
|
||||
(Some(proto::Plan::ZedPro), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Zed Pro subscription."
|
||||
}
|
||||
@@ -1090,7 +1084,7 @@ impl Render for ConfigurationView {
|
||||
"You have basic access to Zed's hosted LLMs through your Zed Free subscription."
|
||||
}
|
||||
_ => {
|
||||
if eligible_for_trial {
|
||||
if self.eligible_for_trial {
|
||||
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
|
||||
} else {
|
||||
"Subscribe for access to Zed's hosted LLMs."
|
||||
@@ -1101,7 +1095,7 @@ impl Render for ConfigurationView {
|
||||
h_flex().child(
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx)))),
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
} else {
|
||||
h_flex()
|
||||
@@ -1109,28 +1103,38 @@ impl Render for ConfigurationView {
|
||||
.child(
|
||||
Button::new("learn_more", "Learn more")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.open_url(ZED_PRICING_URL))),
|
||||
.on_click(|_, _, cx| cx.open_url(ZED_PRICING_URL)),
|
||||
)
|
||||
.child(
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(
|
||||
cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
Button::new(
|
||||
"upgrade",
|
||||
if self.plan.is_none() && self.eligible_for_trial {
|
||||
"Start Trial"
|
||||
} else {
|
||||
"Upgrade"
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
};
|
||||
|
||||
if is_connected {
|
||||
if self.is_connected {
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.children(render_accept_terms(
|
||||
self.state.clone(),
|
||||
LanguageModelProviderTosView::Configuration,
|
||||
cx,
|
||||
))
|
||||
.when(has_accepted_terms, |this| {
|
||||
.when(!self.has_accepted_terms_of_service, |this| {
|
||||
this.child(render_accept_terms(
|
||||
LanguageModelProviderTosView::Configuration,
|
||||
self.accept_terms_of_service_in_progress,
|
||||
{
|
||||
let callback = self.accept_terms_of_service_callback.clone();
|
||||
move |window, cx| (callback)(window, cx)
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(self.has_accepted_terms_of_service, |this| {
|
||||
this.child(subscription_text)
|
||||
.child(manage_subscription_buttons)
|
||||
})
|
||||
@@ -1143,8 +1147,126 @@ impl Render for ConfigurationView {
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, _, cx| this.authenticate(cx))),
|
||||
.on_click({
|
||||
let callback = self.sign_in_callback.clone();
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
state: Entity<State>,
|
||||
accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn new(state: Entity<State>) -> Self {
|
||||
let accept_terms_of_service_callback = Arc::new({
|
||||
let state = state.clone();
|
||||
move |_window: &mut Window, cx: &mut App| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.accept_terms_of_service(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let sign_in_callback = Arc::new({
|
||||
let state = state.clone();
|
||||
move |_window: &mut Window, cx: &mut App| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.authenticate(cx).detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
state,
|
||||
accept_terms_of_service_callback,
|
||||
sign_in_callback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let state = self.state.read(cx);
|
||||
let user_store = state.user_store.read(cx);
|
||||
|
||||
ZedAiConfiguration {
|
||||
is_connected: !state.is_signed_out(),
|
||||
plan: user_store.current_plan(),
|
||||
subscription_period: user_store.subscription_period(),
|
||||
eligible_for_trial: user_store.trial_started_at().is_none(),
|
||||
has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
|
||||
accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(),
|
||||
accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(),
|
||||
sign_in_callback: self.sign_in_callback.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ZedAiConfiguration {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
fn configuration(
|
||||
is_connected: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
eligible_for_trial: bool,
|
||||
has_accepted_terms_of_service: bool,
|
||||
) -> AnyElement {
|
||||
ZedAiConfiguration {
|
||||
is_connected,
|
||||
plan,
|
||||
subscription_period: plan
|
||||
.is_some()
|
||||
.then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
|
||||
eligible_for_trial,
|
||||
has_accepted_terms_of_service,
|
||||
accept_terms_of_service_in_progress: false,
|
||||
accept_terms_of_service_callback: Arc::new(|_, _| {}),
|
||||
sign_in_callback: Arc::new(|_, _| {}),
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example("Not connected", configuration(false, None, false, true)),
|
||||
single_example(
|
||||
"Accept Terms of Service",
|
||||
configuration(true, None, true, false),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Not eligible for trial",
|
||||
configuration(true, None, false, true),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Eligible for trial",
|
||||
configuration(true, None, true, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
configuration(true, Some(proto::Plan::Free), true, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial Plan",
|
||||
configuration(true, Some(proto::Plan::ZedProTrial), true, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Plan",
|
||||
configuration(true, Some(proto::Plan::ZedPro), true, true),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ use super::anthropic::count_anthropic_tokens;
|
||||
use super::google::count_google_tokens;
|
||||
use super::open_ai::count_open_ai_tokens;
|
||||
|
||||
const PROVIDER_ID: &str = "copilot_chat";
|
||||
const PROVIDER_NAME: &str = "GitHub Copilot Chat";
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
|
||||
const PROVIDER_NAME: LanguageModelProviderName =
|
||||
LanguageModelProviderName::new("GitHub Copilot Chat");
|
||||
|
||||
pub struct CopilotChatLanguageModelProvider {
|
||||
state: Entity<State>,
|
||||
@@ -102,11 +103,11 @@ impl LanguageModelProviderState for CopilotChatLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -201,11 +202,11 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
@@ -391,24 +392,24 @@ pub fn map_to_language_model_completion_events(
|
||||
serde_json::Value::from_str(&tool_call.arguments)
|
||||
};
|
||||
match arguments {
|
||||
Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: tool_call.id.clone().into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
},
|
||||
)),
|
||||
Err(error) => {
|
||||
Err(LanguageModelCompletionError::BadInputJson {
|
||||
id: tool_call.id.into(),
|
||||
tool_name: tool_call.name.as_str().into(),
|
||||
raw_input: tool_call.arguments.into(),
|
||||
json_parse_error: error.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: tool_call.id.clone().into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
},
|
||||
)),
|
||||
Err(error) => Ok(
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id: tool_call.id.into(),
|
||||
tool_name: tool_call.name.as_str().into(),
|
||||
raw_input: tool_call.arguments.into(),
|
||||
json_parse_error: error.to_string(),
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ use util::ResultExt;
|
||||
|
||||
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
|
||||
|
||||
const PROVIDER_ID: &str = "deepseek";
|
||||
const PROVIDER_NAME: &str = "DeepSeek";
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
|
||||
const DEEPSEEK_API_KEY_VAR: &str = "DEEPSEEK_API_KEY";
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -174,11 +174,11 @@ impl LanguageModelProviderState for DeepSeekLanguageModelProvider {
|
||||
|
||||
impl LanguageModelProvider for DeepSeekLanguageModelProvider {
|
||||
fn id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
@@ -283,11 +283,11 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
LanguageModelProviderId(PROVIDER_ID.into())
|
||||
PROVIDER_ID
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
LanguageModelProviderName(PROVIDER_NAME.into())
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
@@ -466,7 +466,7 @@ impl DeepSeekEventMapper {
|
||||
events.flat_map(move |event| {
|
||||
futures::stream::iter(match event {
|
||||
Ok(event) => self.map_event(event),
|
||||
Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
|
||||
Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -476,7 +476,7 @@ impl DeepSeekEventMapper {
|
||||
event: deepseek::StreamResponse,
|
||||
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
||||
let Some(choice) = event.choices.first() else {
|
||||
return vec![Err(LanguageModelCompletionError::Other(anyhow!(
|
||||
return vec![Err(LanguageModelCompletionError::from(anyhow!(
|
||||
"Response contained no choices"
|
||||
)))];
|
||||
};
|
||||
@@ -538,8 +538,8 @@ impl DeepSeekEventMapper {
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
},
|
||||
)),
|
||||
Err(error) => Err(LanguageModelCompletionError::BadInputJson {
|
||||
id: tool_call.id.into(),
|
||||
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id: tool_call.id.clone().into(),
|
||||
tool_name: tool_call.name.as_str().into(),
|
||||
raw_input: tool_call.arguments.into(),
|
||||
json_parse_error: error.to_string(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user