diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 0ee6af8a1d..6d8e0107e9 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -19,14 +19,27 @@ self-hosted-runner: - namespace-profile-16x32-ubuntu-2004-arm - namespace-profile-32x64-ubuntu-2004-arm # Namespace Ubuntu 22.04 (Everything else) - - namespace-profile-2x4-ubuntu-2204 - namespace-profile-4x8-ubuntu-2204 - namespace-profile-8x16-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204 + # Namespace Ubuntu 24.04 (like ubuntu-latest) + - namespace-profile-2x4-ubuntu-2404 # Namespace Limited Preview - namespace-profile-8x16-ubuntu-2004-arm-m4 - namespace-profile-8x32-ubuntu-2004-arm-m4 # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 + +# Disable shellcheck because it doesn't like powershell +# This should have been triggered with initial rollout of actionlint +# but https://github.com/zed-industries/zed/pull/36693 +# somehow caused actionlint to actually check those windows jobs +# where previously they were being skipped. Likely caused by an +# unknown bug in actionlint where parsing of `runs-on: [ ]` +# breaks something else. (yuck) +paths: + .github/workflows/{ci,release_nightly}.yml: + ignore: + - "shellcheck" diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index d8eaa6019e..d400905b4d 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -8,7 +8,7 @@ on: jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ba227168..a34833d0fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run_nix: ${{ steps.filter.outputs.run_nix }} run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - - ubuntu-latest + - namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +237,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' needs: [job_spec] steps: @@ -418,7 +418,7 @@ jobs: if: | github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Environment Setup run: | @@ -458,7 +458,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - job_spec - style @@ -784,7 +784,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] if: contains(github.event.pull_request.labels.*.name, 'run-bundling') # if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) needs: [windows_tests] diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 15c82643ae..3f84179278 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ on: jobs: danger: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 0cc6737a45..2026ee7b73 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 60 name: Run tests on Windows if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -206,9 +206,6 @@ jobs: runs-on: github-8vcpu-ubuntu-2404 needs: tests name: Build Zed on FreeBSD - # env: - # MYTOKEN : ${{ secrets.MYTOKEN }} - # MYTOKEN2: "value2" steps: - uses: actions/checkout@v4 - name: Build FreeBSD remote-server @@ -243,7 +240,6 @@ jobs: bundle-nix: name: Build and cache Nix package - if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml @@ -252,7 +248,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} @@ -294,7 +290,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - bundle-mac - bundle-linux-x86 diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index c32a433e46..5dbfc9cb7f 100644 --- a/.github/workflows/script_checks.yml +++ b/.github/workflows/script_checks.yml @@ -12,7 +12,7 @@ jobs: shellcheck: name: "ShellCheck Scripts" if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/Cargo.lock b/Cargo.lock index 6dbbccedaa..06d418a750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,26 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "acp_tools" +version = "0.1.0" +dependencies = [ + "agent-client-protocol", + "collections", + "gpui", + "language", + "markdown", + "project", + "serde", + "serde_json", + "settings", + "theme", + "ui", + "util", + "workspace", + "workspace-hack", +] + [[package]] name = "action_log" version = "0.1.0" @@ -171,11 +191,12 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.26" +version = "0.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" +checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" dependencies = [ "anyhow", + "async-broadcast", "futures 0.3.31", "log", "parking_lot", @@ -239,10 +260,12 @@ dependencies = [ "smol", "sqlez", "task", + "telemetry", "tempfile", "terminal", "text", "theme", + "thiserror 2.0.12", "tree-sitter-rust", "ui", "unindent", @@ -262,16 +285,19 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "acp_tools", "action_log", "agent-client-protocol", "agent_settings", - "agentic-coding-protocol", "anyhow", + "client", "collections", "context_server", "env_logger 0.11.8", + "fs", "futures 0.3.31", "gpui", + "gpui_tokio", "indoc", "itertools 0.14.0", "language", @@ -283,6 +309,7 @@ dependencies = [ "paths", "project", "rand 0.8.5", + "reqwest_client", "schemars", "semver", "serde", @@ -376,6 +403,7 @@ dependencies = [ "parking_lot", "paths", "picker", + "postage", "pretty_assertions", "project", "prompt_store", @@ -415,24 +443,6 @@ dependencies = [ "zed_actions", ] -[[package]] -name = "agentic-coding-protocol" -version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4" -dependencies = [ - "anyhow", - "chrono", - "derive_more 2.0.1", - "futures 0.3.31", - "log", - "parking_lot", - "schemars", - "semver", - "serde", - "serde_json", -] - [[package]] name = "ahash" version = "0.7.8" @@ -848,7 +858,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "extension", "futures 0.3.31", "gpui", @@ -911,7 +921,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more 0.99.19", + "derive_more", "gpui", "icons", "indoc", @@ -948,7 +958,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more 0.99.19", + "derive_more", "diffy", "editor", "feature_flags", @@ -1374,10 +1384,11 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", "gpui", - "parking_lot", "rodio", + "schemars", + "serde", + "settings", "util", "workspace-hack", ] @@ -3060,7 +3071,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more 0.99.19", + "derive_more", "feature_flags", "fs", "futures 0.3.31", @@ -3492,7 +3503,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more 0.99.19", + "derive_more", "gpui", "workspace-hack", ] @@ -4043,6 +4054,7 @@ dependencies = [ name = "crashes" version = "0.1.0" dependencies = [ + "bincode", "crash-handler", "log", "mach2 0.5.0", @@ -4052,6 +4064,7 @@ dependencies = [ "serde", "serde_json", "smol", + "system_specs", "workspace-hack", ] @@ -4653,27 +4666,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", - "unicode-xid", -] - [[package]] name = "derive_refineable" version = "0.1.0" @@ -4694,7 +4686,6 @@ dependencies = [ "component", "ctor", "editor", - "futures 0.3.31", "gpui", "indoc", "language", @@ -5731,14 +5722,10 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ - "client", "editor", "gpui", - "human_bytes", "menu", - "release_channel", - "serde", - "sysinfo", + "system_specs", "ui", "urlencoding", "util", @@ -6414,7 +6401,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "git2", "gpui", @@ -7444,7 +7431,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more 0.99.19", + "derive_more", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7532,6 +7519,7 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ + "anyhow", "gpui", "tokio", "util", @@ -7968,7 +7956,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -8480,6 +8468,7 @@ dependencies = [ "theme", "ui", "util", + "util_macros", "workspace", "workspace-hack", "zed_actions", @@ -9616,6 +9605,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "audio", "collections", "core-foundation 0.10.0", "core-video", @@ -9638,6 +9628,7 @@ dependencies = [ "scap", "serde", "serde_json", + "settings", "sha2", "simplelog", "smallvec", @@ -11624,6 +11615,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "pciid-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" + [[package]] name = "pem" version = "3.0.5" @@ -13524,6 +13521,7 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", + "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", @@ -14363,12 +14361,10 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ - "chrono", "dyn-clone", "indexmap", "ref-cast", "schemars_derive", - "semver", "serde", "serde_json", ] @@ -16144,6 +16140,21 @@ dependencies = [ "winx", ] +[[package]] +name = "system_specs" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "human_bytes", + "pciid-parser", + "release_channel", + "serde", + "sysinfo", + "workspace-hack", +] + [[package]] name = "tab_switcher" version = "0.1.0" @@ -16437,7 +16448,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", + "derive_more", "fs", "futures 0.3.31", "gpui", @@ -19779,7 +19790,6 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", - "bincode", "call", "client", "clock", @@ -19798,6 +19808,7 @@ dependencies = [ "node_runtime", "parking_lot", "postage", + "pretty_assertions", "project", "remote", "schemars", @@ -19953,7 +19964,6 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", - "schemars", "scopeguard", "sea-orm", "sea-query-binder", @@ -20387,8 +20397,9 @@ dependencies = [ [[package]] name = "zed" -version = "0.201.0" +version = "0.202.0" dependencies = [ + "acp_tools", "activity_indicator", "agent", "agent_servers", @@ -20404,6 +20415,7 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", + "bincode", "breadcrumbs", "call", "channel", @@ -20502,6 +20514,7 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", + "system_specs", "tab_switcher", "task", "tasks_ui", diff --git a/Cargo.toml b/Cargo.toml index dc14c8ebd9..6ec243a9b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/acp_tools", "crates/acp_thread", "crates/action_log", "crates/activity_indicator", @@ -155,6 +156,7 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", + "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -226,6 +228,7 @@ edition = "2024" # Workspace member crates # +acp_tools = { path = "crates/acp_tools" } acp_thread = { path = "crates/acp_thread" } action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } @@ -381,6 +384,7 @@ streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } supermaven_api = { path = "crates/supermaven_api" } +system_specs = { path = "crates/system_specs" } tab_switcher = { path = "crates/tab_switcher" } task = { path = "crates/task" } tasks_ui = { path = "crates/tasks_ui" } @@ -422,8 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.26" +agent-client-protocol = "0.0.31" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" @@ -450,6 +453,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" +bincode = "1.2.1" bitflags = "2.6.0" blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } @@ -493,6 +497,7 @@ handlebars = "4.3" heck = "0.5" heed = { version = "0.21.0", features = ["read-txn-no-tls"] } hex = "0.4.3" +human_bytes = "0.4.1" html5ever = "0.27.0" http = "1.1" http-body = "1.0" @@ -532,6 +537,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" partial-json-fixer = "0.5.3" parse_int = "0.9" +pciid-parser = "0.8.0" pathdiff = "0.2" pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } @@ -802,60 +808,32 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so -# warning on this rule produces a lot of noise. -single_range_in_vec_init = "allow" +# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454 +# Remove when the lint gets promoted to `suspicious`. +declare_interior_mutable_const = "deny" -# These are all of the rules that currently have violations in the Zed -# codebase. -# -# We'll want to drive this list down by either: -# 1. fixing violations of the rule and begin enforcing it -# 2. deciding we want to allow the rule permanently, at which point -# we should codify that separately above. -# -# This list shouldn't be added to; it should only get shorter. -# ============================================================================= +redundant_clone = "deny" -# There are a bunch of rules currently failing in the `style` group, so -# allow all of those, for now. +# We currently do not restrict any style rules +# as it slows down shipping code to Zed. +# +# Running ./script/clippy can take several minutes, and so it's +# common to skip that step and let CI do it. Any unexpected failures +# (which also take minutes to discover) thus require switching back +# to an old branch, manual fixing, and re-pushing. +# +# In the future we could improve this by either making sure +# Zed can surface clippy errors in diagnostics (in addition to the +# rust-analyzer errors), or by having CI fix style nits automatically. style = { level = "allow", priority = -1 } -# Temporary list of style lints that we've fixed so far. -comparison_to_empty = "warn" -into_iter_on_ref = "warn" -iter_cloned_collect = "warn" -iter_next_slice = "warn" -iter_nth = "warn" -iter_nth_zero = "warn" -iter_skip_next = "warn" -let_and_return = "warn" -module_inception = { level = "deny" } -question_mark = { level = "deny" } -single_match = "warn" -redundant_closure = { level = "deny" } -redundant_static_lifetimes = { level = "warn" } -redundant_pattern_matching = "warn" -redundant_field_names = "warn" -declare_interior_mutable_const = { level = "deny" } -collapsible_if = { level = "warn"} -collapsible_else_if = { level = "warn" } -needless_borrow = { level = "warn"} -needless_return = { level = "warn" } -unnecessary_mut_passed = {level = "warn"} -unnecessary_map_or = { level = "warn" } -unused_unit = "warn" - # Individual rules that have violations in the codebase: type_complexity = "allow" -# We often return trait objects from `new` functions. -new_ret_no_self = { level = "allow" } -# We have a few `next` functions that differ in lifetimes -# compared to Iterator::next. Yet, clippy complains about those. -should_implement_trait = { level = "allow" } let_underscore_future = "allow" -# It doesn't make sense to implement `Default` unilaterally. -new_without_default = "allow" + +# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so +# warning on this rule produces a lot of noise. +single_range_in_vec_init = "allow" # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. @@ -864,10 +842,6 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" -# `enum_variant_names` fires for all enums, even when they derive serde traits. -# Adhering to this lint would be a breaking change. -enum_variant_names = "allow" - [workspace.metadata.cargo-machete] ignored = [ "bindgen", diff --git a/Procfile.web b/Procfile.web new file mode 100644 index 0000000000..8140555144 --- /dev/null +++ b/Procfile.web @@ -0,0 +1,2 @@ +postgrest_llm: postgrest crates/collab/postgrest_llm.conf +website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/assets/icons/attach.svg b/assets/icons/attach.svg new file mode 100644 index 0000000000..f923a3c7c8 --- /dev/null +++ b/assets/icons/attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index bca13f8d56..aba193930b 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index 87add13216..b9cc19e22f 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg new file mode 100644 index 0000000000..4241d766ac --- /dev/null +++ b/assets/icons/pencil_unavailable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index efd5908a90..773f5e7fa7 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg index b6e120a0b6..0c80e22c51 100644 --- a/assets/icons/zed_agent.svg +++ b/assets/icons/zed_agent.svg @@ -1,27 +1,27 @@ - + - - + + - - + + - + - - - - - - - - + + + + + + + + - + - - + + diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index 470eb0fede..812277a100 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b4efa70572..e84f4834af 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,7 +16,6 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -138,7 +137,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -241,7 +240,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", @@ -856,7 +855,7 @@ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1195,9 +1194,16 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", + "ctrl-enter": "onboarding::Finish", + "alt-shift-l": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ad2ab2ba89..e72f4174ff 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -162,7 +162,7 @@ "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -281,7 +281,7 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", @@ -915,7 +915,7 @@ "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,5 +1301,12 @@ "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 1c381b0cf0..2e27158e11 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index fdf9c437cf..1d723bd75b 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index be6d34a134..62e50b3c8c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -819,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "project_panel::OpenWithSystem", + "s": "workspace::OpenWithSystem", "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", diff --git a/assets/settings/default.json b/assets/settings/default.json index c290baf003..f0b9e11e57 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,6 +162,12 @@ // 2. Always quit the application // "on_last_window_closed": "quit_app", "on_last_window_closed": "platform_default", + // Whether to show padding for zoomed panels. + // When enabled, zoomed center panels (e.g. code editor) will have padding all around, + // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively). + // + // Default: true + "zoomed_padding": true, // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, @@ -1133,11 +1139,6 @@ // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. "max_severity": null - }, - "cargo": { - // When enabled, Zed disables rust-analyzer's check on save and starts to query - // Cargo diagnostics separately. - "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file @@ -1503,6 +1504,11 @@ // // Default: fallback "words": "fallback", + // Minimum number of characters required to automatically trigger word-based completions. + // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + // + // Default: 3 + "words_min_length": 3, // Whether to fetch LSP completions or not. // // Default: true @@ -1629,6 +1635,9 @@ "allowed": true } }, + "Kotlin": { + "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] + }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], @@ -1642,9 +1651,6 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", - "completions": { - "words": "disabled" - }, "prettier": { "allowed": true } @@ -1658,9 +1664,6 @@ } }, "Plain Text": { - "completions": { - "words": "disabled" - }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index f9f8720729..0ffbb9f61e 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#bfbdb6ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#fcfcfcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#cccac2ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 459825c733..f0f0358b76 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#f9f5d7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#f2e5bcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 23ebbcc67e..33f6d3c622 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#dce0e5ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#242529ff", + "terminal.ansi.bright_black": "#747579ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 5d3b35d018..4ded647a74 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -183,16 +183,15 @@ impl ToolCall { language_registry: Arc, cx: &mut App, ) -> Self { + let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { + first_line.to_owned() + "…" + } else { + tool_call.title + }; Self { id: tool_call.id, - label: cx.new(|cx| { - Markdown::new( - tool_call.title.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), + label: cx + .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), kind: tool_call.kind, content: tool_call .content @@ -233,15 +232,30 @@ impl ToolCall { if let Some(title) = title { self.label.update(cx, |label, cx| { - label.replace(title, cx); + if let Some((first_line, _)) = title.split_once("\n") { + label.replace(first_line.to_owned() + "…", cx) + } else { + label.replace(title, cx); + } }); } if let Some(content) = content { - self.content = content - .into_iter() - .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx)) - .collect(); + let new_content_len = content.len(); + let mut content = content.into_iter(); + + // Reuse existing content if we can + for (old, new) in self.content.iter_mut().zip(content.by_ref()) { + old.update_from_acp(new, language_registry.clone(), cx); + } + for new in content { + self.content.push(ToolCallContent::from_acp( + new, + language_registry.clone(), + cx, + )) + } + self.content.truncate(new_content_len); } if let Some(locations) = locations { @@ -301,11 +315,9 @@ impl ToolCall { ) -> Option { let buffer = project .update(cx, |project, cx| { - if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { - Some(project.open_buffer(path, cx)) - } else { - None - } + project + .project_path_for_absolute_path(&location.path, cx) + .map(|path| project.open_buffer(path, cx)) }) .ok()??; let buffer = buffer.await.log_err()?; @@ -471,7 +483,7 @@ impl ContentBlock { fn block_string_contents(&self, block: acp::ContentBlock) -> String { match block { - acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::Text(text_content) => text_content.text, acp::ContentBlock::ResourceLink(resource_link) => { Self::resource_link_md(&resource_link.uri) } @@ -500,7 +512,7 @@ impl ContentBlock { "`Image`".into() } - fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), @@ -553,6 +565,28 @@ impl ToolCallContent { } } + pub fn update_from_acp( + &mut self, + new: acp::ToolCallContent, + language_registry: Arc, + cx: &mut App, + ) { + let needs_update = match (&self, &new) { + (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => { + old_diff.read(cx).needs_update( + new_diff.old_text.as_deref().unwrap_or(""), + &new_diff.new_text, + cx, + ) + } + _ => true, + }; + + if needs_update { + *self = Self::from_acp(new, language_registry, cx); + } + } + pub fn to_markdown(&self, cx: &App) -> String { match self { Self::ContentBlock(content) => content.to_markdown(cx).to_string(), @@ -674,6 +708,37 @@ pub struct TokenUsage { pub used_tokens: u64, } +impl TokenUsage { + pub fn ratio(&self) -> TokenUsageRatio { + #[cfg(debug_assertions)] + let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD") + .unwrap_or("0.8".to_string()) + .parse() + .unwrap(); + #[cfg(not(debug_assertions))] + let warning_threshold: f32 = 0.8; + + // When the maximum is unknown because there is no selected model, + // avoid showing the token limit warning. + if self.max_tokens == 0 { + TokenUsageRatio::Normal + } else if self.used_tokens >= self.max_tokens { + TokenUsageRatio::Exceeded + } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold { + TokenUsageRatio::Warning + } else { + TokenUsageRatio::Normal + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenUsageRatio { + Normal, + Warning, + Exceeded, +} + #[derive(Debug, Clone)] pub struct RetryStatus { pub last_error: SharedString, @@ -694,6 +759,8 @@ pub struct AcpThread { connection: Rc, session_id: acp::SessionId, token_usage: Option, + prompt_capabilities: acp::PromptCapabilities, + _observe_prompt_capabilities: Task>, } #[derive(Debug)] @@ -708,11 +775,12 @@ pub enum AcpThreadEvent { Stopped, Error, LoadError(LoadError), + PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, @@ -759,7 +827,20 @@ impl AcpThread { project: Entity, action_log: Entity, session_id: acp::SessionId, + mut prompt_capabilities_rx: watch::Receiver, + cx: &mut Context, ) -> Self { + let prompt_capabilities = *prompt_capabilities_rx.borrow(); + let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| { + loop { + let caps = prompt_capabilities_rx.recv().await?; + this.update(cx, |this, cx| { + this.prompt_capabilities = caps; + cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated); + })?; + } + }); + Self { action_log, shared_buffers: Default::default(), @@ -771,9 +852,15 @@ impl AcpThread { connection, session_id, token_usage: None, + prompt_capabilities, + _observe_prompt_capabilities: task, } } + pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + pub fn connection(&self) -> &Rc { &self.connection } @@ -958,10 +1045,19 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } - pub fn update_title(&mut self, title: SharedString, cx: &mut Context) -> Result<()> { - self.title = title; - cx.emit(AcpThreadEvent::TitleUpdated); - Ok(()) + pub fn can_set_title(&mut self, cx: &mut Context) -> bool { + self.connection.set_title(&self.session_id, cx).is_some() + } + + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> { + if title != self.title { + self.title = title.clone(); + cx.emit(AcpThreadEvent::TitleUpdated); + if let Some(set_title) = self.connection.set_title(&self.session_id, cx) { + return set_title.run(title, cx); + } + } + Task::ready(Ok(())) } pub fn update_token_usage(&mut self, usage: Option, cx: &mut Context) { @@ -989,7 +1085,7 @@ impl AcpThread { let location_updated = update.fields.locations.is_some(); current_call.update_fields(update.fields, languages, cx); if location_updated { - self.resolve_locations(update.id.clone(), cx); + self.resolve_locations(update.id, cx); } } ToolCallUpdate::UpdateDiff(update) => { @@ -1264,11 +1360,7 @@ impl AcpThread { }; let git_store = self.project.read(cx).git_store().clone(); - let message_id = if self - .connection - .session_editor(&self.session_id, cx) - .is_some() - { + let message_id = if self.connection.truncate(&self.session_id, cx).is_some() { Some(UserMessageId::new()) } else { None @@ -1306,6 +1398,10 @@ impl AcpThread { }) } + pub fn can_resume(&self, cx: &App) -> bool { + self.connection.resume(&self.session_id, cx).is_some() + } + pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { self.run_turn(cx, async move |this, cx| { this.update(cx, |this, cx| { @@ -1352,7 +1448,7 @@ impl AcpThread { let canceled = matches!( result, Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Canceled + stop_reason: acp::StopReason::Cancelled })) ); @@ -1365,6 +1461,17 @@ impl AcpThread { this.send_task.take(); } + // Truncate entries if the last prompt was refused. + if let Ok(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + })) = result + && let Some((ix, _)) = this.last_user_message() + { + let range = ix..this.entries.len(); + this.entries.truncate(ix); + cx.emit(AcpThreadEvent::EntriesRemoved(range)); + } + cx.emit(AcpThreadEvent::Stopped); Ok(()) } @@ -1403,7 +1510,7 @@ impl AcpThread { /// Rewinds this thread to before the entry at `index`, removing it and all /// subsequent entries while reverting any changes made from that point. pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { - let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { + let Some(truncate) = self.connection.truncate(&self.session_id, cx) else { return Task::ready(Err(anyhow!("not supported"))); }; let Some(message) = self.user_message(&id) else { @@ -1423,8 +1530,7 @@ impl AcpThread { .await?; } - cx.update(|cx| session_editor.truncate(id.clone(), cx))? - .await?; + cx.update(|cx| truncate.run(id.clone(), cx))?.await?; this.update(cx, |this, cx| { if let Some((ix, _)) = this.user_message_mut(&id) { let range = ix..this.entries.len(); @@ -2340,6 +2446,92 @@ mod tests { assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } + #[gpui::test] + async fn test_refusal(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/"), json!({})).await; + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + + let refuse_next = Arc::new(AtomicBool::new(false)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let refuse_next = refuse_next.clone(); + move |request, thread, mut cx| { + let refuse_next = refuse_next.clone(); + async move { + if refuse_next.load(SeqCst) { + return Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + }); + } + + let acp::ContentBlock::Text(content) = &request.prompt[0] else { + panic!("expected text content block"); + }; + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentMessageChunk { + content: content.text.to_uppercase().into(), + }, + cx, + ) + .unwrap(); + })?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + .boxed_local() + } + })); + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) + .await + .unwrap(); + + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + + // Simulate refusing the second message, ensuring the conversation gets + // truncated to before sending it. + refuse_next.store(true, SeqCst); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + } + async fn run_until_first_tool_call( thread: &Entity, cx: &mut TestAppContext, @@ -2432,13 +2624,19 @@ mod tests { .into(), ); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert(session_id, thread.downgrade()); @@ -2485,11 +2683,11 @@ mod tests { .detach(); } - fn session_editor( + fn truncate( &self, session_id: &acp::SessionId, - _cx: &mut App, - ) -> Option> { + _cx: &App, + ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), })) @@ -2504,8 +2702,8 @@ mod tests { _session_id: acp::SessionId, } - impl AgentSessionEditor for FakeAgentSessionEditor { - fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { + impl AgentSessionTruncate for FakeAgentSessionEditor { + fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 8cae975ce5..af229b7545 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -41,18 +41,26 @@ pub trait AgentConnection { fn resume( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - fn session_editor( + fn truncate( &self, _session_id: &acp::SessionId, - _cx: &mut App, - ) -> Option> { + _cx: &App, + ) -> Option> { + None + } + + fn set_title( + &self, + _session_id: &acp::SessionId, + _cx: &App, + ) -> Option> { None } @@ -64,6 +72,10 @@ pub trait AgentConnection { None } + fn telemetry(&self) -> Option> { + None + } + fn into_any(self: Rc) -> Rc; } @@ -73,14 +85,31 @@ impl dyn AgentConnection { } } -pub trait AgentSessionEditor { - fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; +pub trait AgentSessionTruncate { + fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; } pub trait AgentSessionResume { fn run(&self, cx: &mut App) -> Task>; } +pub trait AgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task>; +} + +pub trait AgentTelemetry { + /// The name of the agent used for telemetry. + fn agent_name(&self) -> String; + + /// A representation of the current thread state that can be serialized for + /// storage with telemetry events. + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task>; +} + #[derive(Debug)] pub struct AuthRequired { pub description: Option, @@ -298,13 +327,19 @@ mod test_support { ) -> Task>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert( @@ -393,15 +428,15 @@ mod test_support { .response_tx .take() { - end_turn_tx.send(acp::StopReason::Canceled).unwrap(); + end_turn_tx.send(acp::StopReason::Cancelled).unwrap(); } } - fn session_editor( + fn truncate( &self, _session_id: &agent_client_protocol::SessionId, - _cx: &mut App, - ) -> Option> { + _cx: &App, + ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } @@ -412,8 +447,8 @@ mod test_support { struct StubAgentSessionEditor; - impl AgentSessionEditor for StubAgentSessionEditor { - fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> { + impl AgentSessionTruncate for StubAgentSessionEditor { + fn run(&self, _: UserMessageId, _: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 4b779931c5..0fec6809e0 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,57 +28,46 @@ impl Diff { cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); - let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); - let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); - let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); - + let base_text = old_text.clone().unwrap_or(String::new()).into(); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); + let buffer = new_buffer.clone(); async move |_, cx| { let language = language_registry .language_for_file_path(&path) .await .log_err(); - new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; + buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { - buffer.set_language(language, cx); - buffer.snapshot() - })?; - - buffer_diff - .update(cx, |diff, cx| { - diff.set_base_text( - old_buffer_snapshot, - Some(language_registry), - new_buffer_snapshot, - cx, - ) - })? - .await?; + let diff = build_buffer_diff( + old_text.unwrap_or("".into()).into(), + &buffer, + Some(language_registry.clone()), + cx, + ) + .await?; multibuffer .update(cx, |multibuffer, cx| { let hunk_ranges = { - let buffer = new_buffer.read(cx); - let diff = buffer_diff.read(cx); + let buffer = buffer.read(cx); + let diff = diff.read(cx); diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>() }; multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&new_buffer, cx), - new_buffer.clone(), + PathKey::for_buffer(&buffer, cx), + buffer.clone(), hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); - multibuffer.add_diff(buffer_diff, cx); + multibuffer.add_diff(diff, cx); }) .log_err(); @@ -89,23 +78,26 @@ impl Diff { Self::Finalized(FinalizedDiff { multibuffer, path, + base_text, + new_buffer, _update_diff: task, }) } pub fn new(buffer: Entity, cx: &mut Context) -> Self { - let buffer_snapshot = buffer.read(cx).snapshot(); - let base_text = buffer_snapshot.text(); - let language_registry = buffer.read(cx).language_registry(); - let text_snapshot = buffer.read(cx).text_snapshot(); + let buffer_text_snapshot = buffer.read(cx).text_snapshot(); + let base_text_snapshot = buffer.read(cx).snapshot(); + let base_text = base_text_snapshot.text(); + debug_assert_eq!(buffer_text_snapshot.text(), base_text); let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - let _ = diff.set_base_text( - buffer_snapshot.clone(), - language_registry, - text_snapshot, - cx, - ); + let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); + let snapshot = diff.snapshot(cx); + let secondary_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); + diff + }); + diff.set_secondary_diff(secondary_diff); diff }); @@ -123,7 +115,7 @@ impl Diff { diff.update(cx); } }), - buffer, + new_buffer: buffer, diff: buffer_diff, revealed_ranges: Vec::new(), update_diff: Task::ready(Ok(())), @@ -158,9 +150,9 @@ impl Diff { .map(|buffer| buffer.read(cx).text()) .join("\n"); let path = match self { - Diff::Pending(PendingDiff { buffer, .. }) => { - buffer.read(cx).file().map(|file| file.path().as_ref()) - } + Diff::Pending(PendingDiff { + new_buffer: buffer, .. + }) => buffer.read(cx).file().map(|file| file.path().as_ref()), Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), }; format!( @@ -173,12 +165,33 @@ impl Diff { pub fn has_revealed_range(&self, cx: &App) -> bool { self.multibuffer().read(cx).excerpt_paths().next().is_some() } + + pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool { + match self { + Diff::Pending(PendingDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + Diff::Finalized(FinalizedDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + } + } } pub struct PendingDiff { multibuffer: Entity, base_text: Arc, - buffer: Entity, + new_buffer: Entity, diff: Entity, revealed_ranges: Vec>, _subscription: Subscription, @@ -187,7 +200,7 @@ pub struct PendingDiff { impl PendingDiff { pub fn update(&mut self, cx: &mut Context) { - let buffer = self.buffer.clone(); + let buffer = self.new_buffer.clone(); let buffer_diff = self.diff.clone(); let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { @@ -204,7 +217,10 @@ impl PendingDiff { ) .await?; buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &text_snapshot, cx) + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + diff.secondary_diff().unwrap().update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + }); })?; diff.update(cx, |diff, cx| { if let Diff::Pending(diff) = diff { @@ -222,10 +238,10 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.buffer.read(cx).language_registry().clone(); + let language_registry = self.new_buffer.read(cx).language_registry(); let path = self - .buffer + .new_buffer .read(cx) .file() .map(|file| file.path().as_ref()) @@ -234,12 +250,12 @@ impl PendingDiff { // Replace the buffer in the multibuffer with the snapshot let buffer = cx.new(|cx| { - let language = self.buffer.read(cx).language().cloned(); + let language = self.new_buffer.read(cx).language().cloned(); let buffer = TextBuffer::new_normalized( 0, cx.entity_id().as_non_zero_u64().into(), - self.buffer.read(cx).line_ending(), - self.buffer.read(cx).as_rope().clone(), + self.new_buffer.read(cx).line_ending(), + self.new_buffer.read(cx).as_rope().clone(), ); let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); buffer.set_language(language, cx); @@ -248,7 +264,6 @@ impl PendingDiff { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, language_registry, cx).await } @@ -276,7 +291,9 @@ impl PendingDiff { FinalizedDiff { path, + base_text: self.base_text.clone(), multibuffer: self.multibuffer.clone(), + new_buffer: self.new_buffer.clone(), _update_diff: update_diff, } } @@ -285,8 +302,8 @@ impl PendingDiff { let ranges = self.excerpt_ranges(cx); self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&self.buffer, cx), - self.buffer.clone(), + PathKey::for_buffer(&self.new_buffer, cx), + self.new_buffer.clone(), ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, @@ -298,7 +315,7 @@ impl PendingDiff { } fn excerpt_ranges(&self, cx: &App) -> Vec> { - let buffer = self.buffer.read(cx); + let buffer = self.new_buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) @@ -332,6 +349,8 @@ impl PendingDiff { pub struct FinalizedDiff { path: PathBuf, + base_text: Arc, + new_buffer: Entity, multibuffer: Entity, _update_diff: Task>, } @@ -385,3 +404,21 @@ async fn build_buffer_diff( diff }) } + +#[cfg(test)] +mod tests { + use gpui::{AppContext as _, TestAppContext}; + use language::Buffer; + + use crate::Diff; + + #[gpui::test] + async fn test_pending_diff(cx: &mut TestAppContext) { + let buffer = cx.new(|cx| Buffer::local("hello!", cx)); + let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| { + buffer.set_text("HELLO!", cx); + }); + cx.run_until_parked(); + } +} diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index a1e713cffa..6fa0887e22 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId}; use serde::{Deserialize, Serialize}; use std::{ fmt, - ops::Range, + ops::RangeInclusive, path::{Path, PathBuf}, str::FromStr, }; @@ -17,13 +17,14 @@ pub enum MentionUri { File { abs_path: PathBuf, }, + PastedImage, Directory { abs_path: PathBuf, }, Symbol { - path: PathBuf, + abs_path: PathBuf, name: String, - line_range: Range, + line_range: RangeInclusive, }, Thread { id: acp::SessionId, @@ -38,8 +39,9 @@ pub enum MentionUri { name: String, }, Selection { - path: PathBuf, - line_range: Range, + #[serde(default, skip_serializing_if = "Option::is_none")] + abs_path: Option, + line_range: RangeInclusive, }, Fetch { url: Url, @@ -48,36 +50,44 @@ pub enum MentionUri { impl MentionUri { pub fn parse(input: &str) -> Result { + fn parse_line_range(fragment: &str) -> Result> { + let range = fragment + .strip_prefix("L") + .context("Line range must start with \"L\"")?; + let (start, end) = range + .split_once(":") + .context("Line range must use colon as separator")?; + let range = start + .parse::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..=end + .parse::() + .context("Parsing line range end")? + .checked_sub(1) + .context("Line numbers should be 1-based")?; + Ok(range) + } + let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { - let range = fragment - .strip_prefix("L") - .context("Line range must start with \"L\"")?; - let (start, end) = range - .split_once(":") - .context("Line range must use colon as separator")?; - let line_range = start - .parse::() - .context("Parsing line range start")? - .checked_sub(1) - .context("Line numbers should be 1-based")? - ..end - .parse::() - .context("Parsing line range end")? - .checked_sub(1) - .context("Line numbers should be 1-based")?; + let line_range = parse_line_range(fragment)?; if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - path, + abs_path: path, line_range, }) } else { - Ok(Self::Selection { path, line_range }) + Ok(Self::Selection { + abs_path: Some(path), + line_range, + }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path }) @@ -105,6 +115,17 @@ impl MentionUri { id: rule_id.into(), name, }) + } else if path.starts_with("/agent/pasted-image") { + Ok(Self::PastedImage) + } else if path.starts_with("/agent/untitled-buffer") { + let fragment = url + .fragment() + .context("Missing fragment for untitled buffer selection")?; + let line_range = parse_line_range(fragment)?; + Ok(Self::Selection { + abs_path: None, + line_range, + }) } else { bail!("invalid zed url: {:?}", input); } @@ -121,13 +142,16 @@ impl MentionUri { .unwrap_or_default() .to_string_lossy() .into_owned(), + MentionUri::PastedImage => "Image".to_string(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::TextThread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Selection { - path, line_range, .. - } => selection_name(path, line_range), + abs_path: path, + line_range, + .. + } => selection_name(path.as_deref(), line_range), MentionUri::Fetch { url } => url.to_string(), } } @@ -137,6 +161,7 @@ impl MentionUri { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } + MentionUri::PastedImage => IconName::Image.path().into(), MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), @@ -157,29 +182,40 @@ impl MentionUri { MentionUri::File { abs_path } => { Url::from_file_path(abs_path).expect("mention path should be absolute") } + MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), MentionUri::Directory { abs_path } => { Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { - path, + abs_path, name, line_range, } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + let mut url = + Url::from_file_path(abs_path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } - MentionUri::Selection { path, line_range } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + let mut url = if let Some(path) = path { + Url::from_file_path(path).expect("mention path should be absolute") + } else { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path("/agent/untitled-buffer"); + url + }; url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } @@ -191,7 +227,10 @@ impl MentionUri { } MentionUri::TextThread { path, name } => { let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); + url.set_path(&format!( + "/agent/text-thread/{}", + path.to_string_lossy().trim_start_matches('/') + )); url.query_pairs_mut().append_pair("name", name); url } @@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result> { } } -pub fn selection_name(path: &Path, line_range: &Range) -> String { +pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { format!( "{} ({}:{})", - path.file_name().unwrap_or_default().display(), - line_range.start + 1, - line_range.end + 1 + path.and_then(|path| path.file_name()) + .unwrap_or("Untitled".as_ref()) + .display(), + *line_range.start() + 1, + *line_range.end() + 1 ) } @@ -302,14 +343,14 @@ mod tests { let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { - path, + abs_path: path, name, line_range, } => { assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); - assert_eq!(line_range.start, 9); - assert_eq!(line_range.end, 19); + assert_eq!(line_range.start(), &9); + assert_eq!(line_range.end(), &19); } _ => panic!("Expected Symbol variant"), } @@ -321,16 +362,39 @@ mod tests { let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { - MentionUri::Selection { path, line_range } => { - assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); - assert_eq!(line_range.start, 4); - assert_eq!(line_range.end, 14); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + assert_eq!( + path.as_ref().unwrap().to_str().unwrap(), + path!("/path/to/file.rs") + ); + assert_eq!(line_range.start(), &4); + assert_eq!(line_range.end(), &14); } _ => panic!("Expected Selection variant"), } assert_eq!(parsed.to_uri().to_string(), selection_uri); } + #[test] + fn test_parse_untitled_selection_uri() { + let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10"); + let parsed = MentionUri::parse(selection_uri).unwrap(); + match &parsed { + MentionUri::Selection { + abs_path: None, + line_range, + } => { + assert_eq!(line_range.start(), &0); + assert_eq!(line_range.end(), &9); + } + _ => panic!("Expected Selection variant without path"), + } + assert_eq!(parsed.to_uri().to_string(), selection_uri); + } + #[test] fn test_parse_thread_uri() { let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml new file mode 100644 index 0000000000..7a6d8c21a0 --- /dev/null +++ b/crates/acp_tools/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "acp_tools" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + + +[lints] +workspace = true + +[lib] +path = "src/acp_tools.rs" +doctest = false + +[dependencies] +agent-client-protocol.workspace = true +collections.workspace = true +gpui.workspace = true +language.workspace= true +markdown.workspace = true +project.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +workspace-hack.workspace = true +workspace.workspace = true diff --git a/crates/acp_tools/LICENSE-GPL b/crates/acp_tools/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/acp_tools/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs new file mode 100644 index 0000000000..ee12b04cde --- /dev/null +++ b/crates/acp_tools/src/acp_tools.rs @@ -0,0 +1,494 @@ +use std::{ + cell::RefCell, + collections::HashSet, + fmt::Display, + rc::{Rc, Weak}, + sync::Arc, +}; + +use agent_client_protocol as acp; +use collections::HashMap; +use gpui::{ + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, + StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, +}; +use language::LanguageRegistry; +use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; +use project::Project; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; +use util::ResultExt as _; +use workspace::{Item, Workspace}; + +actions!(acp, [OpenDebugTools]); + +pub fn init(cx: &mut App) { + cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { + workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { + let acp_tools = + Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); + workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); + }); + }, + ) + .detach(); +} + +struct GlobalAcpConnectionRegistry(Entity); + +impl Global for GlobalAcpConnectionRegistry {} + +#[derive(Default)] +pub struct AcpConnectionRegistry { + active_connection: RefCell>, +} + +struct ActiveConnection { + server_name: SharedString, + connection: Weak, +} + +impl AcpConnectionRegistry { + pub fn default_global(cx: &mut App) -> Entity { + if cx.has_global::() { + cx.global::().0.clone() + } else { + let registry = cx.new(|_cx| AcpConnectionRegistry::default()); + cx.set_global(GlobalAcpConnectionRegistry(registry.clone())); + registry + } + } + + pub fn set_active_connection( + &self, + server_name: impl Into, + connection: &Rc, + cx: &mut Context, + ) { + self.active_connection.replace(Some(ActiveConnection { + server_name: server_name.into(), + connection: Rc::downgrade(connection), + })); + cx.notify(); + } +} + +struct AcpTools { + project: Entity, + focus_handle: FocusHandle, + expanded: HashSet, + watched_connection: Option, + connection_registry: Entity, + _subscription: Subscription, +} + +struct WatchedConnection { + server_name: SharedString, + messages: Vec, + list_state: ListState, + connection: Weak, + incoming_request_methods: HashMap>, + outgoing_request_methods: HashMap>, + _task: Task<()>, +} + +impl AcpTools { + fn new(project: Entity, cx: &mut Context) -> Self { + let connection_registry = AcpConnectionRegistry::default_global(cx); + + let subscription = cx.observe(&connection_registry, |this, _, cx| { + this.update_connection(cx); + cx.notify(); + }); + + let mut this = Self { + project, + focus_handle: cx.focus_handle(), + expanded: HashSet::default(), + watched_connection: None, + connection_registry, + _subscription: subscription, + }; + this.update_connection(cx); + this + } + + fn update_connection(&mut self, cx: &mut Context) { + let active_connection = self.connection_registry.read(cx).active_connection.borrow(); + let Some(active_connection) = active_connection.as_ref() else { + return; + }; + + if let Some(watched_connection) = self.watched_connection.as_ref() { + if Weak::ptr_eq( + &watched_connection.connection, + &active_connection.connection, + ) { + return; + } + } + + if let Some(connection) = active_connection.connection.upgrade() { + let mut receiver = connection.subscribe(); + let task = cx.spawn(async move |this, cx| { + while let Ok(message) = receiver.recv().await { + this.update(cx, |this, cx| { + this.push_stream_message(message, cx); + }) + .ok(); + } + }); + + self.watched_connection = Some(WatchedConnection { + server_name: active_connection.server_name.clone(), + messages: vec![], + list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), + connection: active_connection.connection.clone(), + incoming_request_methods: HashMap::default(), + outgoing_request_methods: HashMap::default(), + _task: task, + }); + } + } + + fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context) { + let Some(connection) = self.watched_connection.as_mut() else { + return; + }; + let language_registry = self.project.read(cx).languages().clone(); + let index = connection.messages.len(); + + let (request_id, method, message_type, params) = match stream_message.message { + acp::StreamMessageContent::Request { id, method, params } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.incoming_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.outgoing_request_methods + } + }; + + method_map.insert(id, method.clone()); + (Some(id), method.into(), MessageType::Request, Ok(params)) + } + acp::StreamMessageContent::Response { id, result } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.outgoing_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.incoming_request_methods + } + }; + + if let Some(method) = method_map.remove(&id) { + (Some(id), method.into(), MessageType::Response, result) + } else { + ( + Some(id), + "[unrecognized response]".into(), + MessageType::Response, + result, + ) + } + } + acp::StreamMessageContent::Notification { method, params } => { + (None, method.into(), MessageType::Notification, Ok(params)) + } + }; + + let message = WatchedConnectionMessage { + name: method, + message_type, + request_id, + direction: stream_message.direction, + collapsed_params_md: match params.as_ref() { + Ok(params) => params + .as_ref() + .map(|params| collapsed_params_md(params, &language_registry, cx)), + Err(err) => { + if let Ok(err) = &serde_json::to_value(err) { + Some(collapsed_params_md(&err, &language_registry, cx)) + } else { + None + } + } + }, + + expanded_params_md: None, + params, + }; + + connection.messages.push(message); + connection.list_state.splice(index..index, 1); + cx.notify(); + } + + fn render_message( + &mut self, + index: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let Some(connection) = self.watched_connection.as_ref() else { + return Empty.into_any(); + }; + + let Some(message) = connection.messages.get(index) else { + return Empty.into_any(); + }; + + let base_size = TextSize::Editor.rems(cx); + + let theme_settings = ThemeSettings::get_global(cx); + let text_style = window.text_style(); + + let colors = cx.theme().colors(); + let expanded = self.expanded.contains(&index); + + v_flex() + .w_full() + .px_4() + .py_3() + .border_color(colors.border) + .border_b_1() + .gap_2() + .items_start() + .font_buffer(cx) + .text_size(base_size) + .id(index) + .group("message") + .hover(|this| this.bg(colors.element_background.opacity(0.5))) + .on_click(cx.listener(move |this, _, _, cx| { + if this.expanded.contains(&index) { + this.expanded.remove(&index); + } else { + this.expanded.insert(index); + let Some(connection) = &mut this.watched_connection else { + return; + }; + let Some(message) = connection.messages.get_mut(index) else { + return; + }; + message.expanded(this.project.read(cx).languages().clone(), cx); + connection.list_state.scroll_to_reveal_item(index); + } + cx.notify() + })) + .child( + h_flex() + .w_full() + .gap_2() + .items_center() + .flex_shrink_0() + .child(match message.direction { + acp::StreamMessageDirection::Incoming => { + ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error) + } + acp::StreamMessageDirection::Outgoing => { + ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success) + } + }) + .child( + Label::new(message.name.clone()) + .buffer_font(cx) + .color(Color::Muted), + ) + .child(div().flex_1()) + .child( + div() + .child(ui::Chip::new(message.message_type.to_string())) + .visible_on_hover("message"), + ) + .children( + message + .request_id + .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))), + ), + ) + // I'm aware using markdown is a hack. Trying to get something working for the demo. + // Will clean up soon! + .when_some( + if expanded { + message.expanded_params_md.clone() + } else { + message.collapsed_params_md.clone() + }, + |this, params| { + this.child( + div().pl_6().w_full().child( + MarkdownElement::new( + params, + MarkdownStyle { + base_text_style: text_style, + selection_background_color: colors.element_selection_background, + syntax: cx.theme().syntax().clone(), + code_block_overflow_x_scroll: true, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some( + theme_settings.buffer_font.family.clone(), + ), + font_size: Some((base_size * 0.8).into()), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ) + .code_block_renderer( + CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: expanded, + border: false, + }, + ), + ), + ) + }, + ) + .into_any() + } +} + +struct WatchedConnectionMessage { + name: SharedString, + request_id: Option, + direction: acp::StreamMessageDirection, + message_type: MessageType, + params: Result, acp::Error>, + collapsed_params_md: Option>, + expanded_params_md: Option>, +} + +impl WatchedConnectionMessage { + fn expanded(&mut self, language_registry: Arc, cx: &mut App) { + let params_md = match &self.params { + Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)), + Err(err) => { + if let Some(err) = &serde_json::to_value(err).log_err() { + Some(expanded_params_md(&err, &language_registry, cx)) + } else { + None + } + } + _ => None, + }; + self.expanded_params_md = params_md; + } +} + +fn collapsed_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string(params).unwrap_or_default(); + let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4); + + for ch in params_json.chars() { + match ch { + '{' => spaced_out_json.push_str("{ "), + '}' => spaced_out_json.push_str(" }"), + ':' => spaced_out_json.push_str(": "), + ',' => spaced_out_json.push_str(", "), + c => spaced_out_json.push(c), + } + } + + let params_md = format!("```json\n{}\n```", spaced_out_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +fn expanded_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string_pretty(params).unwrap_or_default(); + let params_md = format!("```json\n{}\n```", params_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +enum MessageType { + Request, + Response, + Notification, +} + +impl Display for MessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageType::Request => write!(f, "Request"), + MessageType::Response => write!(f, "Response"), + MessageType::Notification => write!(f, "Notification"), + } + } +} + +enum AcpToolsEvent {} + +impl EventEmitter for AcpTools {} + +impl Item for AcpTools { + type Event = AcpToolsEvent; + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString { + format!( + "ACP: {}", + self.watched_connection + .as_ref() + .map_or("Disconnected", |connection| &connection.server_name) + ) + .into() + } + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + Some(ui::Icon::new(IconName::Thread)) + } +} + +impl Focusable for AcpTools { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for AcpTools { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .track_focus(&self.focus_handle) + .size_full() + .bg(cx.theme().colors().editor_background) + .child(match self.watched_connection.as_ref() { + Some(connection) => { + if connection.messages.is_empty() { + h_flex() + .size_full() + .justify_center() + .items_center() + .child("No messages recorded yet") + .into_any() + } else { + list( + connection.list_state.clone(), + cx.processor(Self::render_message), + ) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any() + } + } + None => h_flex() + .size_full() + .justify_center() + .items_center() + .child("No active connection") + .into_any(), + }) + } +} diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 1c3cad386d..9ec10f4dbb 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -161,7 +161,7 @@ impl ActionLog { diff_base, last_seen_base, unreviewed_edits, - snapshot: text_snapshot.clone(), + snapshot: text_snapshot, status, version: buffer.read(cx).version(), diff, @@ -190,7 +190,7 @@ impl ActionLog { cx: &mut Context, ) { match event { - BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), + BufferEvent::Edited => self.handle_buffer_edited(buffer, cx), BufferEvent::FileHandleChanged => { self.handle_buffer_file_changed(buffer, cx); } @@ -461,7 +461,7 @@ impl ActionLog { anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry().clone(), + buffer.read(cx).language_registry(), )) })??; let diff_snapshot = BufferDiff::update_diff( @@ -529,12 +529,12 @@ impl ActionLog { /// Mark a buffer as created by agent, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer.clone(), true, cx); + self.track_buffer_internal(buffer, true, cx); } /// Mark a buffer as edited by agent, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); + let tracked_buffer = self.track_buffer_internal(buffer, false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } @@ -2425,7 +2425,7 @@ mod tests { assert_eq!( unreviewed_hunks(&action_log, cx), vec![( - buffer.clone(), + buffer, vec![ HunkStatus { range: Point::new(6, 0)..Point::new(7, 0), diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 324480f5b4..6641db0805 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -104,7 +104,7 @@ impl ActivityIndicator { &workspace_handle, window, |activity_indicator, _, event, window, cx| { - if let workspace::Event::ClearActivityIndicator { .. } = event + if let workspace::Event::ClearActivityIndicator = event && activity_indicator.statuses.pop().is_some() { activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 1636508df6..c9e73372f6 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -132,7 +132,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -169,7 +169,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -202,7 +202,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 9bb8fc0eae..a94a933d86 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -362,7 +362,7 @@ impl Display for DirectoryContext { let mut is_first = true; for descendant in &self.descendants { if !is_first { - write!(f, "\n")?; + writeln!(f)?; } else { is_first = false; } @@ -650,7 +650,7 @@ impl TextThreadContextHandle { impl Display for TextThreadContext { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // TODO: escape title? - write!(f, "\n", self.title)?; + writeln!(f, "", self.title)?; write!(f, "{}", self.text.trim())?; write!(f, "\n") } @@ -716,7 +716,7 @@ impl RulesContextHandle { impl Display for RulesContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(title) = &self.title { - write!(f, "Rules title: {}\n", title)?; + writeln!(f, "Rules title: {}", title)?; } let code_block = MarkdownCodeBlock { tag: "", diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 22d1a72bf5..696c569356 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -86,15 +86,13 @@ impl Tool for ContextServerTool { ) -> ToolResult { if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) { let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index eb39c3e454..8f4c1a1e2e 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -254,10 +254,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::Thread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index fc91e1bb62..899e360ab0 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -181,7 +181,7 @@ impl Message { } } - pub fn to_string(&self) -> String { + pub fn to_message_content(&self) -> String { let mut result = String::new(); if !self.loaded_context.text.is_empty() { @@ -387,7 +387,6 @@ pub struct Thread { cumulative_token_usage: TokenUsage, exceeded_window_error: Option, tool_use_limit_reached: bool, - feedback: Option, retry_state: Option, message_feedback: HashMap, last_received_chunk_at: Option, @@ -487,14 +486,13 @@ impl Thread { cumulative_token_usage: TokenUsage::default(), exceeded_window_error: None, tool_use_limit_reached: false, - feedback: None, retry_state: None, message_feedback: HashMap::default(), last_error_context: None, last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, - configured_model: configured_model.clone(), + configured_model, profile: AgentProfile::new(profile_id, tools), } } @@ -532,7 +530,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) @@ -612,7 +610,6 @@ impl Thread { cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, tool_use_limit_reached: serialized.tool_use_limit_reached, - feedback: None, message_feedback: HashMap::default(), last_error_context: None, last_received_chunk_at: None, @@ -667,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { + if self.configured_model.is_none() || self.messages.is_empty() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -1646,10 +1643,10 @@ impl Thread { }; self.tool_use - .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); + .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx); self.tool_use.insert_tool_output( - tool_use_id.clone(), + tool_use_id, tool_name, tool_output, self.configured_model.as_ref(), @@ -2100,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { println!("No thread summary model"); return; }; @@ -2419,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() + LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { return; }; @@ -2787,10 +2784,6 @@ impl Thread { cx.emit(ThreadEvent::CancelEditing); } - pub fn feedback(&self) -> Option { - self.feedback - } - pub fn message_feedback(&self, message_id: MessageId) -> Option { self.message_feedback.get(&message_id).copied() } @@ -2823,7 +2816,7 @@ impl Thread { let message_content = self .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); cx.background_spawn(async move { @@ -2852,52 +2845,6 @@ impl Thread { }) } - pub fn report_feedback( - &mut self, - feedback: ThreadFeedback, - cx: &mut Context, - ) -> Task> { - let last_assistant_message_id = self - .messages - .iter() - .rev() - .find(|msg| msg.role == Role::Assistant) - .map(|msg| msg.id); - - if let Some(message_id) = last_assistant_message_id { - self.report_message_feedback(message_id, feedback, cx) - } else { - let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx); - let serialized_thread = self.serialize(cx); - let thread_id = self.id().clone(); - let client = self.project.read(cx).client(); - self.feedback = Some(feedback); - cx.notify(); - - cx.background_spawn(async move { - let final_project_snapshot = final_project_snapshot.await; - let serialized_thread = serialized_thread.await?; - let thread_data = serde_json::to_value(serialized_thread) - .unwrap_or_else(|_| serde_json::Value::Null); - - let rating = match feedback { - ThreadFeedback::Positive => "positive", - ThreadFeedback::Negative => "negative", - }; - telemetry::event!( - "Assistant Thread Rated", - rating, - thread_id, - thread_data, - final_project_snapshot - ); - client.telemetry().flush_events().await; - - Ok(()) - }) - } - } - /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity, @@ -3241,7 +3188,7 @@ impl Thread { self.configured_model.as_ref(), self.completion_mode, ); - self.tool_finished(tool_use_id.clone(), None, true, window, cx); + self.tool_finished(tool_use_id, None, true, window, cx); } } @@ -3873,7 +3820,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some(model.provider_id().0.to_string().into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3893,7 +3840,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: None, - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3933,7 +3880,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some("anthropic".into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -5463,13 +5410,10 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); + registry.set_thread_summary_model(Some(ConfiguredModel { + provider, + model: model.clone(), + })); }) }); diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 45e551dbdf..cba2457566 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,8 +893,19 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { Connection::open_file(&sqlite_path.to_string_lossy()) }; diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 2a39440af8..68246a96b0 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -10,6 +10,7 @@ path = "src/agent2.rs" [features] test-support = ["db/test-support"] +e2e = [] [lints] workspace = true @@ -26,6 +27,7 @@ assistant_context.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true chrono.workspace = true +client.workspace = true cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true @@ -57,7 +59,9 @@ settings.workspace = true smol.workspace = true sqlez.workspace = true task.workspace = true +telemetry.workspace = true terminal.workspace = true +thiserror.workspace = true text.workspace = true ui.workspace = true util.workspace = true @@ -70,6 +74,7 @@ zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } +agent_servers = { workspace = true, "features" = ["test-support"] } assistant_context = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3c605de803..ecfaea4b49 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ -use crate::HistoryStore; use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; +use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -180,7 +180,7 @@ impl NativeAgent { fs: Arc, cx: &mut AsyncApp, ) -> Result> { - log::info!("Creating new NativeAgent"); + log::debug!("Creating new NativeAgent"); let project_context = cx .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); + let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -240,21 +240,26 @@ impl NativeAgent { let title = thread.title(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); - let acp_thread = cx.new(|_cx| { + let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); + let acp_thread = cx.new(|cx| { acp_thread::AcpThread::new( title, connection, project.clone(), action_log.clone(), session_id.clone(), + prompt_capabilities_rx, + cx, ) }); let subscriptions = vec![ cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); }), + cx.subscribe(&thread_handle, Self::handle_thread_title_updated), + cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { - this.save_thread(thread.clone(), cx) + this.save_thread(thread, cx) }), ]; @@ -440,6 +445,43 @@ impl NativeAgent { }) } + fn handle_thread_title_updated( + &mut self, + thread: Entity, + _: &TitleUpdated, + cx: &mut Context, + ) { + let session_id = thread.read(cx).id(); + let Some(session) = self.sessions.get(session_id) else { + return; + }; + let thread = thread.downgrade(); + let acp_thread = session.acp_thread.clone(); + cx.spawn(async move |_, cx| { + let title = thread.read_with(cx, |thread, _| thread.title())?; + let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; + task.await + }) + .detach_and_log_err(cx); + } + + fn handle_thread_token_usage_updated( + &mut self, + thread: Entity, + usage: &TokenUsageUpdated, + cx: &mut Context, + ) { + let Some(session) = self.sessions.get(thread.read(cx).id()) else { + return; + }; + session + .acp_thread + .update(cx, |acp_thread, cx| { + acp_thread.update_token_usage(usage.0.clone(), cx); + }) + .ok(); + } + fn handle_project_event( &mut self, _project: Entity, @@ -481,8 +523,8 @@ impl NativeAgent { self.models.refresh_list(cx); let registry = LanguageModelRegistry::read_global(cx); - let default_model = registry.default_model().map(|m| m.model.clone()); - let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); + let default_model = registry.default_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { @@ -559,6 +601,10 @@ impl NativeAgent { } fn save_thread(&mut self, thread: Entity, cx: &mut Context) { + if thread.read(cx).is_empty() { + return; + } + let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); @@ -695,15 +741,6 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } - ThreadEvent::TokenUsageUpdate(usage) => { - acp_thread.update(cx, |thread, cx| { - thread.update_token_usage(Some(usage), cx) - })?; - } - ThreadEvent::TitleUpdate(title) => { - acp_thread - .update(cx, |thread, cx| thread.update_title(title, cx))??; - } ThreadEvent::Retry(status) => { acp_thread.update(cx, |thread, cx| { thread.update_retry_status(status, cx) @@ -722,7 +759,7 @@ impl NativeAgentConnection { } } - log::info!("Response stream completed"); + log::debug!("Response stream completed"); anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) @@ -747,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection { model_id: acp_thread::AgentModelId, cx: &mut App, ) -> Task> { - log::info!("Setting model for session {}: {}", session_id, model_id); + log::debug!("Setting model for session {}: {}", session_id, model_id); let Some(thread) = self .0 .read(cx) @@ -818,12 +855,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Task>> { let agent = self.0.clone(); - log::info!("Creating new thread for project at: {:?}", cwd); + log::debug!("Creating new thread for project at: {:?}", cwd); cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); - let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?; // Create Thread let thread = agent.update( cx, @@ -839,20 +875,16 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - - let thread = cx.new(|cx| { + Ok(cx.new(|cx| { Thread::new( project.clone(), agent.project_context.clone(), agent.context_server_registry.clone(), - action_log.clone(), agent.templates.clone(), default_model, cx, ) - }); - - Ok(thread) + })) }, )??; agent.update(cx, |agent, cx| agent.register_session(thread, cx)) @@ -888,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .into_iter() .map(Into::into) .collect::>(); - log::info!("Converted prompt to message: {} chars", content.len()); + log::debug!("Converted prompt to message: {} chars", content.len()); log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); @@ -899,7 +931,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn resume( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(NativeAgentSessionResume { connection: self.clone(), @@ -916,12 +948,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }); } - fn session_editor( + fn truncate( &self, session_id: &agent_client_protocol::SessionId, - cx: &mut App, - ) -> Option> { - self.0.update(cx, |agent, _cx| { + cx: &App, + ) -> Option> { + self.0.read_with(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { thread: session.thread.clone(), @@ -931,18 +963,54 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn set_title( + &self, + session_id: &acp::SessionId, + _cx: &App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionSetTitle { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + + fn telemetry(&self) -> Option> { + Some(Rc::new(self.clone()) as Rc) + } + fn into_any(self: Rc) -> Rc { self } } +impl acp_thread::AgentTelemetry for NativeAgentConnection { + fn agent_name(&self) -> String { + "Zed".into() + } + + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task> { + let Some(session) = self.0.read(cx).sessions.get(session_id) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + + let task = session.thread.read(cx).to_db(cx); + cx.background_spawn(async move { + serde_json::to_value(task.await).context("Failed to serialize thread") + }) + } +} + struct NativeAgentSessionEditor { thread: Entity, acp_thread: WeakEntity, } -impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { - fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { +impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { + fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { match self.thread.update(cx, |thread, cx| { thread.truncate(message_id.clone(), cx)?; Ok(thread.latest_token_usage()) @@ -974,14 +1042,37 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume { } } +struct NativeAgentSessionSetTitle { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task> { + let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { + return Task::ready(Err(anyhow!("session not found"))); + }; + let thread = session.thread.clone(); + thread.update(cx, |thread, cx| thread.set_title(title, cx)); + Task::ready(Ok(())) + } +} + #[cfg(test)] mod tests { + use crate::HistoryEntryId; + use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; + use acp_thread::{ + AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri, + }; use fs::FakeFs; use gpui::TestAppContext; + use indoc::indoc; + use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; + use util::path; #[gpui::test] async fn test_maintaining_project_context(cx: &mut TestAppContext) { @@ -1166,6 +1257,158 @@ mod tests { ); } + #[gpui::test] + #[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows + async fn test_save_load_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": { + "b.md": "Lorem" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let agent = NativeAgent::new( + project.clone(), + history_store.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = Rc::new(NativeAgentConnection(agent.clone())); + + let acp_thread = cx + .update(|cx| { + connection + .clone() + .new_thread(project.clone(), Path::new(""), cx) + }) + .await + .unwrap(); + let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); + let thread = agent.read_with(cx, |agent, _| { + agent.sessions.get(&session_id).unwrap().thread.clone() + }); + + // Ensure empty threads are not saved, even if they get mutated. + let model = Arc::new(FakeLanguageModel::default()); + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_model(model.clone(), cx); + thread.set_summarization_model(Some(summary_model.clone()), cx); + }); + cx.run_until_parked(); + assert_eq!(history_entries(&history_store, cx), vec![]); + + let send = acp_thread.update(cx, |thread, cx| { + thread.send( + vec![ + "What does ".into(), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: "b.md".into(), + uri: MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri() + .to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + " mean?".into(), + ], + cx, + ) + }); + let send = cx.foreground_executor().spawn(send); + cx.run_until_parked(); + + model.send_last_completion_stream_text_chunk("Lorem."); + model.end_last_completion_stream(); + cx.run_until_parked(); + summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md"); + summary_model.end_last_completion_stream(); + + send.await.unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + + cx.run_until_parked(); + + // Drop the ACP thread, which should cause the session to be dropped as well. + cx.update(|_| { + drop(thread); + drop(acp_thread); + }); + agent.read_with(cx, |agent, _| { + assert_eq!(agent.sessions.keys().cloned().collect::>(), []); + }); + + // Ensure the thread can be reloaded from disk. + assert_eq!( + history_entries(&history_store, cx), + vec![( + HistoryEntryId::AcpThread(session_id.clone()), + "Explaining /a/b.md".into() + )] + ); + let acp_thread = agent + .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) + .await + .unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + } + + fn history_entries( + history: &Entity, + cx: &mut TestAppContext, + ) -> Vec<(HistoryEntryId, String)> { + history.read_with(cx, |history, _| { + history + .entries() + .map(|e| (e.id(), e.title().to_string())) + .collect::>() + }) + } + fn init_test(cx: &mut TestAppContext) { env_logger::try_init().ok(); cx.update(|cx| { diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index c6a6c38201..e7d31c0c7a 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -266,8 +266,19 @@ impl ThreadsDatabase { } pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { let threads_dir = paths::data_dir().join("threads"); std::fs::create_dir_all(&threads_dir)?; @@ -287,7 +298,7 @@ impl ThreadsDatabase { .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; let db = Self { - executor: executor.clone(), + executor, connection: Arc::new(Mutex::new(connection)), }; @@ -325,7 +336,7 @@ impl ThreadsDatabase { INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) "})?; - insert((id.0.clone(), title, updated_at, data_type, data))?; + insert((id.0, title, updated_at, data_type, data))?; Ok(()) } @@ -434,7 +445,7 @@ mod tests { let client = Client::new(clock, http_client, cx); agent::init(cx); agent_settings::init(cx); - language_model::init(client.clone(), cx); + language_model::init(client, cx); }); } diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 7eb7da94ba..c656456e01 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -10,6 +10,7 @@ use itertools::Itertools; use paths::contexts_dir; use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use ui::ElementId; use util::ResultExt as _; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; @@ -68,6 +69,15 @@ pub enum HistoryEntryId { TextThread(Arc), } +impl Into for HistoryEntryId { + fn into(self) -> ElementId { + match self { + HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()), + HistoryEntryId::TextThread(path) => ElementId::Path(path), + } + } +} + #[derive(Serialize, Deserialize, Debug)] enum SerializedRecentOpen { AcpThread(String), @@ -76,6 +86,7 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, + entries: Vec, context_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, @@ -87,7 +98,7 @@ impl HistoryStore { context_store: Entity, cx: &mut Context, ) -> Self { - let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; + let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await; @@ -106,11 +117,16 @@ impl HistoryStore { context_store, recently_opened_entries: VecDeque::default(), threads: Vec::default(), + entries: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), } } + pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> { + self.threads.iter().find(|thread| &thread.id == session_id) + } + pub fn delete_thread( &mut self, id: acp::SessionId, @@ -167,20 +183,18 @@ impl HistoryStore { } } this.threads = threads; - cx.notify(); + this.update_entries(cx); }) }) .detach_and_log_err(cx); } - pub fn entries(&self, cx: &App) -> Vec { - let mut history_entries = Vec::new(); - + fn update_entries(&mut self, cx: &mut Context) { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return history_entries; + return; } - + let mut history_entries = Vec::new(); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend( self.context_store @@ -191,17 +205,12 @@ impl HistoryStore { ); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); - history_entries + self.entries = history_entries; + cx.notify() } - pub fn is_empty(&self, cx: &App) -> bool { - self.threads.is_empty() - && self - .context_store - .read(cx) - .unordered_contexts() - .next() - .is_none() + pub fn is_empty(&self, _cx: &App) -> bool { + self.entries.is_empty() } pub fn recently_opened_entries(&self, cx: &App) -> Vec { @@ -312,10 +321,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } @@ -342,4 +350,8 @@ impl HistoryStore { .retain(|old_entry| old_entry != entry); self.save_recently_opened_entries(cx); } + + pub fn entries(&self) -> impl Iterator { + self.entries.iter().cloned() + } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 74d24efb13..9ff98ccd18 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; use fs::Fs; -use gpui::{App, Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; use project::Project; use prompt_store::PromptStore; @@ -22,16 +22,20 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { - fn name(&self) -> &'static str { - "Native Agent" + fn telemetry_id(&self) -> &'static str { + "zed" } - fn empty_state_headline(&self) -> &'static str { - "" + fn name(&self) -> SharedString { + "Zed Agent".into() } - fn empty_state_message(&self) -> &'static str { - "" + fn empty_state_headline(&self) -> SharedString { + self.name() + } + + fn empty_state_message(&self) -> SharedString { + "".into() } fn logo(&self) -> ui::IconName { @@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer { project: &Entity, cx: &mut App, ) -> Task>> { - log::info!( + log::debug!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); @@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer { // Create the connection wrapper let connection = NativeAgentConnection(agent); - log::info!("NativeAgentServer connection established successfully"); + log::debug!("NativeAgentServer connection established successfully"); Ok(Rc::new(connection) as Rc) }) @@ -73,3 +77,52 @@ impl AgentServer for NativeAgentServer { self } } + +#[cfg(test)] +mod tests { + use super::*; + + use assistant_context::ContextStore; + use gpui::AppContext; + + agent_servers::e2e_tests::common_e2e_tests!( + async |fs, project, cx| { + let auth = cx.update(|cx| { + prompt_store::init(cx); + terminal::init(cx); + + let registry = language_model::LanguageModelRegistry::read_global(cx); + let auth = registry + .provider(&language_model::ANTHROPIC_PROVIDER_ID) + .unwrap() + .authenticate(cx); + + cx.spawn(async move |_| auth.await) + }); + + auth.await.unwrap(); + + cx.update(|cx| { + let registry = language_model::LanguageModelRegistry::global(cx); + + registry.update(cx, |registry, cx| { + registry.select_default_model( + Some(&language_model::SelectedModel { + provider: language_model::ANTHROPIC_PROVIDER_ID, + model: language_model::LanguageModelId("claude-sonnet-4-latest".into()), + }), + cx, + ); + }); + }); + + let history = cx.update(|cx| { + let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx)); + cx.new(move |cx| HistoryStore::new(context_store, cx)) + }); + + NativeAgentServer::new(fs.clone(), history) + }, + allow_option_id = "allow" + ); +} diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 55bfa6f0b5..864fbf8b10 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,30 +1,40 @@ use super::*; use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; -use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; +use cloud_llm_client::CompletionIntent; +use collections::IndexMap; +use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; -use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; +use futures::{ + StreamExt, + channel::{ + mpsc::{self, UnboundedReceiver}, + oneshot, + }, +}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, - fake_provider::FakeLanguageModel, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat, + LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; -use project::Project; +use project::{ + Project, context_server_store::ContextServerStore, project_settings::ProjectSettings, +}; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -32,17 +42,22 @@ mod test_tools; use test_tools::*; #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -57,9 +72,9 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { @@ -74,9 +89,18 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking { + text: "Think".to_string(), + signature: None, + }); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -210,7 +234,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: json!({"text": "test"}).to_string(), input: json!({"text": "test"}), is_input_complete: true, @@ -223,7 +247,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "test".into(), output: Some("test".into()), @@ -271,7 +295,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_basic_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -293,7 +317,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { // Test a tool calls that's likely to complete *after* streaming stops. let events = thread .update(cx, |thread, cx| { - thread.remove_tool(&AgentTool::name(&EchoTool)); + thread.remove_tool(&EchoTool::name()); thread.add_tool(DelayTool); thread.send( UserMessageId::new(), @@ -331,7 +355,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -397,7 +421,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_1".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -406,7 +430,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_2".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -437,14 +461,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![ language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) }), language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), output: None @@ -456,7 +480,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_3".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -478,7 +502,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -490,7 +514,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_4".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -505,7 +529,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -557,7 +581,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { cx.run_until_parked(); let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, @@ -570,7 +594,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -650,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { "} ) }); - - // Ensure we error if calling resume when tool use limit was *not* reached. - let error = thread - .update(cx, |thread, cx| thread.resume(cx)) - .unwrap_err(); - assert_eq!( - error.to_string(), - "can only resume after tool use limit is reached" - ) } #[gpui::test] @@ -676,14 +691,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, }; let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -794,7 +809,7 @@ async fn next_tool_call_authorization( } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -860,14 +875,14 @@ async fn test_profiles(cx: &mut TestAppContext) { "test-1": { "name": "Test Profile 1", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, } }, "test-2": { "name": "Test Profile 2", "tools": { - InfiniteTool.name(): true, + InfiniteTool::name(): true, } } } @@ -896,7 +911,7 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]); + assert_eq!(tool_names, vec![DelayTool::name(), EchoTool::name()]); fake_model.end_last_completion_stream(); // Switch to test-2 profile, and verify that it has only the infinite tool. @@ -915,11 +930,339 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![InfiniteTool.name()]); + assert_eq!(tool_names, vec![InfiniteTool::name()]); } #[gpui::test] -#[ignore = "can't run on CI yet"] +async fn test_mcp_tools(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Override profiles and wait for settings to be loaded. + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())) + }); + + let mut mcp_tool_calls = setup_context_server( + "test_server", + vec![context_server::types::Tool { + name: "echo".into(), + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }], + &context_server_store, + cx, + ); + + let events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hey"], cx).unwrap() + }); + cx.run_until_parked(); + + // Simulate the model calling the MCP tool. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_1".into(), + name: "echo".into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { + text: "test".into(), + }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_text_chunk("Done!"); + fake_model.end_last_completion_stream(); + events.collect::>().await; + + // Send again after adding the echo tool, ensuring the name collision is resolved. + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Go"], cx).unwrap() + }); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec!["echo", "test_server_echo"] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_2".into(), + name: "test_server_echo".into(), + raw_input: json!({"text": "mcp"}).to_string(), + input: json!({"text": "mcp"}), + is_input_complete: true, + }, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_3".into(), + name: "echo".into(), + raw_input: json!({"text": "native"}).to_string(), + input: json!({"text": "native"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + // Ensure the tool results were inserted with the correct names. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages.last().unwrap().content, + vec![ + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_3".into(), + tool_name: "echo".into(), + is_error: false, + content: "native".into(), + output: Some("native".into()), + },), + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_2".into(), + tool_name: "test_server_echo".into(), + is_error: false, + content: "mcp".into(), + output: Some("mcp".into()), + },), + ] + ); + fake_model.end_last_completion_stream(); + events.collect::>().await; +} + +#[gpui::test] +async fn test_mcp_tool_truncation(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Set up a profile with all tools enabled + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())); + thread.add_tool(EchoTool); + thread.add_tool(DelayTool); + thread.add_tool(WordListTool); + thread.add_tool(ToolRequiringPermission); + thread.add_tool(InfiniteTool); + }); + + // Set up multiple context servers with some overlapping tool names + let _server1_calls = setup_context_server( + "xxx", + vec![ + context_server::types::Tool { + name: "echo".into(), // Conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_1".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + let _server2_calls = setup_context_server( + "yyy", + vec![ + context_server::types::Tool { + name: "echo".into(), // Also conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_2".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + let _server3_calls = setup_context_server( + "zzz", + vec![ + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Go"], cx) + }) + .unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec![ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "delay", + "echo", + "infinite", + "tool_requiring_permission", + "unique_tool_1", + "unique_tool_2", + "word_list", + "xxx_echo", + "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "yyy_echo", + "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ] + ); +} + +#[gpui::test] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -975,7 +1318,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { assert!( matches!( last_event, - Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) ), "unexpected event {last_event:?}" ); @@ -1029,7 +1372,7 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); let events_1 = events_1.collect::>().await; - assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]); + assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]); let events_2 = events_2.collect::>().await; assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); } @@ -1342,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1369,6 +1713,7 @@ async fn test_title_generation(cx: &mut TestAppContext) { summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); summary_model.end_last_completion_stream(); send.collect::>().await; + cx.run_until_parked(); thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); // Send another message, ensuring no title is generated this time. @@ -1386,6 +1731,81 @@ async fn test_title_generation(cx: &mut TestAppContext) { thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); } +#[gpui::test] +async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let _events = thread + .update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Hey!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let permission_tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: ToolRequiringPermission::name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }; + let echo_tool_use = LanguageModelToolUse { + id: "tool_id_2".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_text_chunk("Hi!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + permission_tool_use, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + echo_tool_use.clone(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Ensure pending tools are skipped when building a request. + let request = thread + .read_with(cx, |thread, cx| { + thread.build_completion_request(CompletionIntent::EditFile, cx) + }) + .unwrap(); + assert_eq!( + request.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Hey!".into()], + cache: true + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![ + MessageContent::Text("Hi!".into()), + MessageContent::ToolUse(echo_tool_use.clone()) + ], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: echo_tool_use.id.clone(), + tool_name: echo_tool_use.name, + is_error: false, + content: "test".into(), + output: Some("test".into()) + })], + cache: false + }, + ], + ); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); @@ -1400,11 +1820,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); Project::init_settings(cx); - LanguageModelRegistry::test(cx); agent_settings::init(cx); + language_model::init(client.clone(), cx); + language_models::init(user_store, client.clone(), cx); + LanguageModelRegistry::test(cx); }); cx.executor().forbid_parking(); @@ -1537,7 +1957,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "1".into(), - name: ThinkingTool.name().into(), + name: ThinkingTool::name().into(), raw_input: input.to_string(), input, is_input_complete: false, @@ -1678,6 +2098,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { .unwrap(); cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey,"); fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { provider: LanguageModelProviderName::new("Anthropic"), retry_after: Some(Duration::from_secs(3)), @@ -1687,8 +2108,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(3)); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.send_last_completion_stream_text_chunk("there!"); fake_model.end_last_completion_stream(); + cx.run_until_parked(); let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { @@ -1716,12 +2138,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { ## Assistant - Hey! + Hey, + + [resume] + + ## Assistant + + there! "} ) }); } +#[gpui::test] +async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let tool_use_1 = LanguageModelToolUse { + id: "tool_1".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + tool_use_1.clone(), + )); + fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }); + fake_model.end_last_completion_stream(); + + cx.executor().advance_clock(Duration::from_secs(3)); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Call the echo tool!".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![language_model::MessageContent::ToolResult( + LanguageModelToolResult { + tool_use_id: tool_use_1.id.clone(), + tool_name: tool_use_1.name.clone(), + is_error: false, + content: "test".into(), + output: Some("test".into()) + } + )], + cache: true + }, + ] + ); + + fake_model.send_last_completion_stream_text_chunk("Done"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + events.collect::>().await; + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.last_message(), + Some(Message::Agent(AgentMessage { + content: vec![AgentMessageContent::Text("Done".into())], + tool_results: IndexMap::default() + })) + ); + }) +} + #[gpui::test] async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; @@ -1792,12 +2296,12 @@ struct ThreadTest { model: Arc, thread: Entity, project_context: Entity, + context_server_store: Entity, fs: Arc, } enum TestModel { Sonnet4, - Sonnet4Thinking, Fake, } @@ -1805,7 +2309,6 @@ impl TestModel { fn id(&self) -> LanguageModelId { match self { TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), - TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()), TestModel::Fake => unreachable!(), } } @@ -1827,11 +2330,12 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { "test-profile": { "name": "Test Profile", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, - WordListTool.name(): true, - ToolRequiringPermission.name(): true, - InfiniteTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, + ThinkingTool::name(): true, } } } @@ -1854,7 +2358,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); watch_settings(fs.clone(), cx); }); @@ -1888,15 +2392,14 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { .await; let project_context = cx.new(|_cx| ProjectContext::default()); + let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); + cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); let thread = cx.new(|cx| { Thread::new( project, project_context.clone(), context_server_registry, - action_log, templates, Some(model.clone()), cx, @@ -1906,6 +2409,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { model, thread, project_context, + context_server_store, fs, } } @@ -1940,3 +2444,89 @@ fn watch_settings(fs: Arc, cx: &mut App) { }) .detach(); } + +fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec { + completion + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect() +} + +fn setup_context_server( + name: &'static str, + tools: Vec, + context_server_store: &Entity, + cx: &mut TestAppContext, +) -> mpsc::UnboundedReceiver<( + context_server::types::CallToolParams, + oneshot::Sender, +)> { + cx.update(|cx| { + let mut settings = ProjectSettings::get_global(cx).clone(); + settings.context_servers.insert( + name.into(), + project::project_settings::ContextServerSettings::Custom { + enabled: true, + command: ContextServerCommand { + path: "somebinary".into(), + args: Vec::new(), + env: None, + }, + }, + ); + ProjectSettings::override_global(settings, cx); + }); + + let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded(); + let fake_transport = context_server::test::create_fake_transport(name, cx.executor()) + .on_request::(move |_params| async move { + context_server::types::InitializeResponse { + protocol_version: context_server::types::ProtocolVersion( + context_server::types::LATEST_PROTOCOL_VERSION.to_string(), + ), + server_info: context_server::types::Implementation { + name: name.into(), + version: "1.0.0".to_string(), + }, + capabilities: context_server::types::ServerCapabilities { + tools: Some(context_server::types::ToolsCapabilities { + list_changed: Some(true), + }), + ..Default::default() + }, + meta: None, + } + }) + .on_request::(move |_params| { + let tools = tools.clone(); + async move { + context_server::types::ListToolsResponse { + tools, + next_cursor: None, + meta: None, + } + } + }) + .on_request::(move |params| { + let mcp_tool_calls_tx = mcp_tool_calls_tx.clone(); + async move { + let (response_tx, response_rx) = oneshot::channel(); + mcp_tool_calls_tx + .unbounded_send((params, response_tx)) + .unwrap(); + response_rx.await.unwrap() + } + }); + context_server_store.update(cx, |store, cx| { + store.start_server( + Arc::new(ContextServer::new( + ContextServerId(name.into()), + Arc::new(fake_transport), + )), + cx, + ); + }); + cx.run_until_parked(); + mcp_tool_calls_rx +} diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index cbff44cedf..27be7b6ac3 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -16,11 +16,11 @@ impl AgentTool for EchoTool { type Input = EchoToolInput; type Output = String; - fn name(&self) -> SharedString { - "echo".into() + fn name() -> &'static str { + "echo" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -51,8 +51,8 @@ impl AgentTool for DelayTool { type Input = DelayToolInput; type Output = String; - fn name(&self) -> SharedString { - "delay".into() + fn name() -> &'static str { + "delay" } fn initial_title(&self, input: Result) -> SharedString { @@ -63,7 +63,7 @@ impl AgentTool for DelayTool { } } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission { type Input = ToolRequiringPermissionInput; type Output = String; - fn name(&self) -> SharedString { - "tool_requiring_permission".into() + fn name() -> &'static str { + "tool_requiring_permission" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool { type Input = InfiniteToolInput; type Output = String; - fn name(&self) -> SharedString { - "infinite".into() + fn name() -> &'static str { + "infinite" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -178,11 +178,11 @@ impl AgentTool for WordListTool { type Input = WordListInput; type Output = String; - fn name(&self) -> SharedString { - "word_list".into() + fn name() -> &'static str { + "word_list" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c1778bf38b..1b1c014b79 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -9,14 +9,15 @@ use action_log::ActionLog; use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; use agent_settings::{ - AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, + AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, + SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; -use collections::{HashMap, IndexMap}; +use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; +use collections::{HashMap, HashSet, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -25,7 +26,9 @@ use futures::{ stream::FuturesUnordered, }; use git::repository::DiffType; -use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, +}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, @@ -42,17 +45,19 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; +use std::fmt::Write; use std::{ collections::BTreeMap, + ops::RangeInclusive, path::Path, sync::Arc, time::{Duration, Instant}, }; -use std::{fmt::Write, ops::Range}; -use util::{ResultExt, markdown::MarkdownCodeBlock}; +use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; +pub const MAX_TOOL_NAME_LENGTH: usize = 64; /// The ID of the user prompt that initiated a request. /// @@ -118,7 +123,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resumed after tool use limit was reached]".into(), + Message::Resume => "[resume]\n".into(), } } @@ -158,9 +163,9 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { if !content.is_empty() { - let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); + let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); } else { - let _ = write!(&mut markdown, "{}\n", uri.as_link()); + let _ = writeln!(&mut markdown, "{}", uri.as_link()); } } } @@ -183,6 +188,7 @@ impl UserMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_DIRECTORIES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_SELECTIONS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = @@ -191,6 +197,7 @@ impl UserMessage { let mut file_context = OPEN_FILES_TAG.to_string(); let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); @@ -207,7 +214,7 @@ impl UserMessage { match uri { MentionUri::File { abs_path } => { write!( - &mut symbol_context, + &mut file_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(abs_path, None), @@ -216,17 +223,19 @@ impl UserMessage { ) .ok(); } + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be used in mention content") + } MentionUri::Directory { .. } => { write!(&mut directory_context, "\n{}\n", content).ok(); } MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. + abs_path: path, + line_range, + .. } => { write!( - &mut rules_context, + &mut symbol_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(path, Some(line_range)), @@ -235,6 +244,24 @@ impl UserMessage { ) .ok(); } + MentionUri::Selection { + abs_path: path, + line_range, + .. + } => { + write!( + &mut selection_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag( + path.as_deref().unwrap_or("Untitled".as_ref()), + Some(line_range) + ), + text: content + } + ) + .ok(); + } MentionUri::Thread { .. } => { write!(&mut thread_context, "\n{}\n", content).ok(); } @@ -287,6 +314,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(symbol_context)); } + if selection_context.len() > OPEN_SELECTIONS_TAG.len() { + selection_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(selection_context)); + } + if thread_context.len() > OPEN_THREADS_TAG.len() { thread_context.push_str("\n"); message @@ -322,7 +356,7 @@ impl UserMessage { } } -fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { +fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { let mut result = String::new(); if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { @@ -332,10 +366,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { let _ = write!(result, "{}", full_path.display()); if let Some(range) = line_range { - if range.start == range.end { - let _ = write!(result, ":{}", range.start + 1); + if range.start() == range.end() { + let _ = write!(result, ":{}", range.start() + 1); } else { - let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); + let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); } } @@ -414,24 +448,33 @@ impl AgentMessage { cache: false, }; for chunk in &self.content { - let chunk = match chunk { + match chunk { AgentMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) + assistant_message + .content + .push(language_model::MessageContent::Text(text.clone())); } AgentMessageContent::Thinking { text, signature } => { - language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - } + assistant_message + .content + .push(language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + }); } AgentMessageContent::RedactedThinking(value) => { - language_model::MessageContent::RedactedThinking(value.clone()) + assistant_message.content.push( + language_model::MessageContent::RedactedThinking(value.clone()), + ); } - AgentMessageContent::ToolUse(value) => { - language_model::MessageContent::ToolUse(value.clone()) + AgentMessageContent::ToolUse(tool_use) => { + if self.tool_results.contains_key(&tool_use.id) { + assistant_message + .content + .push(language_model::MessageContent::ToolUse(tool_use.clone())); + } } }; - assistant_message.content.push(chunk); } let mut user_message = LanguageModelRequestMessage { @@ -484,8 +527,6 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - TokenUsageUpdate(acp_thread::TokenUsage), - TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -497,11 +538,22 @@ pub struct ToolCallAuthorization { pub response: oneshot::Sender, } +#[derive(Debug, thiserror::Error)] +enum CompletionError { + #[error("max tokens")] + MaxTokens, + #[error("refusal")] + Refusal, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + pub struct Thread { id: acp::SessionId, prompt_id: PromptId, updated_at: DateTime, title: Option, + pending_title_generation: Option>, summary: Option, messages: Vec, completion_mode: CompletionMode, @@ -523,26 +575,40 @@ pub struct Thread { templates: Arc, model: Option>, summarization_model: Option>, + prompt_capabilities_tx: watch::Sender, + pub(crate) prompt_capabilities_rx: watch::Receiver, pub(crate) project: Entity, pub(crate) action_log: Entity, } impl Thread { + fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { + let image = model.map_or(true, |model| model.supports_images()); + acp::PromptCapabilities { + image, + audio: false, + embedded_context: true, + } + } + pub fn new( project: Entity, project_context: Entity, context_server_registry: Entity, - action_log: Entity, templates: Arc, model: Option>, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); + let action_log = cx.new(|_cx| ActionLog::new(project.clone())); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), updated_at: Utc::now(), title: None, + pending_title_generation: None, summary: None, messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, @@ -564,6 +630,8 @@ impl Thread { templates, model, summarization_model: None, + prompt_capabilities_tx, + prompt_capabilities_rx, project, action_log, } @@ -614,7 +682,20 @@ impl Thread { stream: &ThreadEventStream, cx: &mut Context, ) { - let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { + let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| { + self.context_server_registry + .read(cx) + .servers() + .find_map(|(_, tools)| { + if let Some(tool) = tools.get(tool_use.name.as_ref()) { + Some(tool.clone()) + } else { + None + } + }) + }); + + let Some(tool) = tool else { stream .0 .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { @@ -677,13 +758,15 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) .or_else(|| registry.default_model()) .map(|model| model.model) }); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id, @@ -693,6 +776,7 @@ impl Thread { } else { Some(db_thread.title.clone()) }, + pending_title_generation: None, summary: db_thread.detailed_summary, messages: db_thread.messages, completion_mode: db_thread.completion_mode.unwrap_or_default(), @@ -712,13 +796,15 @@ impl Thread { project, action_log, updated_at: db_thread.updated_at, + prompt_capabilities_tx, + prompt_capabilities_rx, } } pub fn to_db(&self, cx: &App) -> Task { let initial_project_snapshot = self.initial_project_snapshot.clone(); let mut thread = DbThread { - title: self.title.clone().unwrap_or_default(), + title: self.title(), messages: self.messages.clone(), updated_at: self.updated_at, detailed_summary: self.summary.clone(), @@ -868,15 +954,30 @@ impl Thread { &self.action_log } + pub fn is_empty(&self) -> bool { + self.messages.is_empty() && self.title.is_none() + } + pub fn model(&self) -> Option<&Arc> { self.model.as_ref() } pub fn set_model(&mut self, model: Arc, cx: &mut Context) { + let old_usage = self.latest_token_usage(); self.model = Some(model); + let new_caps = Self::prompt_capabilities(self.model.as_deref()); + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } + self.prompt_capabilities_tx.send(new_caps).log_err(); cx.notify() } + pub fn summarization_model(&self) -> Option<&Arc> { + self.summarization_model.as_ref() + } + pub fn set_summarization_model( &mut self, model: Option>, @@ -891,7 +992,12 @@ impl Thread { } pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context) { + let old_usage = self.latest_token_usage(); self.completion_mode = mode; + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } cx.notify() } @@ -927,11 +1033,11 @@ impl Thread { )); self.add_tool(TerminalTool::new(self.project.clone(), cx)); self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. + self.add_tool(WebSearchTool); } - pub fn add_tool(&mut self, tool: impl AgentTool) { - self.tools.insert(tool.name(), tool.erase()); + pub fn add_tool(&mut self, tool: T) { + self.tools.insert(T::name().into(), tool.erase()); } pub fn remove_tool(&mut self, name: &str) -> bool { @@ -953,13 +1059,15 @@ impl Thread { self.flush_pending_message(cx); } - pub fn update_token_usage(&mut self, update: language_model::TokenUsage) { + fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context) { let Some(last_user_message) = self.last_user_message() else { return; }; self.request_token_usage .insert(last_user_message.id.clone(), update); + cx.emit(TokenUsageUpdated(self.latest_token_usage())); + cx.notify(); } pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context) -> Result<()> { @@ -998,15 +1106,10 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { - anyhow::ensure!( - self.tool_use_limit_reached, - "can only resume after tool use limit is reached" - ); - self.messages.push(Message::Resume); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1024,7 +1127,7 @@ impl Thread { { let model = self.model().context("No language model configured")?; - log::info!("Thread::send called with model: {:?}", model.name()); + log::info!("Thread::send called with model: {}", model.name().0); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -1034,7 +1137,7 @@ impl Thread { .push(Message::User(UserMessage { id, content })); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1045,6 +1148,10 @@ impl Thread { self.cancel(cx); let model = self.model.clone().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("Profile not found")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); @@ -1052,102 +1159,32 @@ impl Thread { self.summary = None; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), + tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { - log::info!("Starting agent turn execution"); - let turn_result: Result = async { - let mut completion_intent = CompletionIntent::UserPrompt; - loop { - log::debug!( - "Building completion request with intent: {:?}", - completion_intent - ); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; + log::debug!("Starting agent turn execution"); - log::info!("Calling model.stream_completion"); - - let mut tool_use_limit_reached = false; - let mut refused = false; - let mut reached_max_tokens = false; - let mut tool_uses = Self::stream_completion_with_retries( - this.clone(), - model.clone(), - request, - &event_stream, - &mut tool_use_limit_reached, - &mut refused, - &mut reached_max_tokens, - cx, - ) - .await?; - - if refused { - return Ok(StopReason::Refusal); - } else if reached_max_tokens { - return Ok(StopReason::MaxTokens); - } - - let end_turn = tool_uses.is_empty(); - while let Some(tool_result) = tool_uses.next().await { - log::info!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); - } - - if tool_use_limit_reached { - log::info!("Tool use limit reached, completing turn"); - this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; - return Err(language_model::ToolUseLimitReachedError.into()); - } else if end_turn { - log::info!("No tool uses found, completing turn"); - return Ok(StopReason::EndTurn); - } else { - this.update(cx, |this, cx| this.flush_pending_message(cx))?; - completion_intent = CompletionIntent::ToolResults; - } - } - } - .await; + let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); match turn_result { - Ok(reason) => { - log::info!("Turn execution completed: {:?}", reason); - - let update_title = this - .update(cx, |this, cx| this.update_title(&event_stream, cx)) - .ok() - .flatten(); - if let Some(update_title) = update_title { - update_title.await.context("update title failed").log_err(); - } - - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } + Ok(()) => { + log::debug!("Turn execution completed"); + event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); + match error.downcast::() { + Ok(CompletionError::Refusal) => { + event_stream.send_stop(acp::StopReason::Refusal); + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + Ok(CompletionError::MaxTokens) => { + event_stream.send_stop(acp::StopReason::MaxTokens); + } + Ok(CompletionError::Other(error)) | Err(error) => { + event_stream.send_error(error); + } + } } } @@ -1157,145 +1194,154 @@ impl Thread { Ok(events_rx) } - async fn stream_completion_with_retries( - this: WeakEntity, + async fn run_turn_internal( + this: &WeakEntity, model: Arc, - request: LanguageModelRequest, event_stream: &ThreadEventStream, - tool_use_limit_reached: &mut bool, - refusal: &mut bool, - max_tokens_reached: &mut bool, cx: &mut AsyncApp, - ) -> Result>> { - log::debug!("Stream completion started successfully"); + ) -> Result<()> { + let mut attempt = 0; + let mut intent = CompletionIntent::UserPrompt; + loop { + let request = + this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; - let mut attempt = None; - 'retry: loop { - let mut events = model.stream_completion(request.clone(), cx).await?; - let mut tool_uses = FuturesUnordered::new(); + telemetry::event!( + "Agent Thread Completion", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt + ); + + log::debug!("Calling model.stream_completion, attempt {}", attempt); + let mut events = model + .stream_completion(request, cx) + .await + .map_err(|error| anyhow!(error))?; + let mut tool_results = FuturesUnordered::new(); + let mut error = None; while let Some(event) = events.next().await { + log::trace!("Received completion event: {:?}", event); match event { - Ok(LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - )) => { - *tool_use_limit_reached = true; - } - Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { - let usage = acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode( - request - .mode - .unwrap_or(cloud_llm_client::CompletionMode::Normal), - ), - used_tokens: token_usage.total_tokens(), - }; - - this.update(cx, |this, _cx| this.update_token_usage(token_usage)) - .ok(); - - event_stream.send_token_usage_update(usage); - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { - *refusal = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)) => { - *max_tokens_reached = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop( - StopReason::ToolUse | StopReason::EndTurn, - )) => break, Ok(event) => { - log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - event_stream, - cx, - )); - }) - .ok(); + tool_results.extend(this.update(cx, |this, cx| { + this.handle_completion_event(event, event_stream, cx) + })??); } - Err(error) => { - let completion_mode = - this.read_with(cx, |thread, _cx| thread.completion_mode())?; - if completion_mode == CompletionMode::Normal { - return Err(error.into()); - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(error.into()); - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - let attempt = attempt.get_or_insert(0u8); - - *attempt += 1; - - let attempt = *attempt; - if attempt > max_attempts { - return Err(error.into()); - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = - initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - event_stream.send_retry(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }); - - cx.background_executor().timer(delay).await; - continue 'retry; + Err(err) => { + error = Some(err); + break; } } } - return Ok(tool_uses); + let end_turn = tool_results.is_empty(); + while let Some(tool_result) = tool_results.next().await { + log::debug!("Tool finished {:?}", tool_result); + + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + })?; + } + + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); + } + })?; + + if let Some(error) = error { + attempt += 1; + let retry = + this.update(cx, |this, _| this.handle_completion_error(error, attempt))??; + let timer = cx.background_executor().timer(retry.duration); + event_stream.send_retry(retry); + timer.await; + this.update(cx, |this, _cx| { + if let Some(Message::Agent(message)) = this.messages.last() { + if message.tool_results.is_empty() { + intent = CompletionIntent::UserPrompt; + this.messages.push(Message::Resume); + } + } + })?; + } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { + return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + attempt = 0; + } } } - pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { - log::debug!("Building system message"); - let prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), + fn handle_completion_error( + &mut self, + error: LanguageModelCompletionError, + attempt: u8, + ) -> Result { + if self.completion_mode == CompletionMode::Normal { + return Err(anyhow!(error)); } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - log::debug!("System message built"); - LanguageModelRequestMessage { - role: Role::System, - content: vec![prompt.into()], - cache: true, + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error)); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + if attempt > max_attempts { + return Err(anyhow!(error)); } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + Ok(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }) } /// A helper method that's called on every streamed completion event. - /// Returns an optional tool result task, which the main agentic loop in - /// send will send back to the model when it resolves. - fn handle_streamed_completion_event( + /// Returns an optional tool result task, which the main agentic loop will + /// send back to the model when it resolves. + fn handle_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, cx: &mut Context, - ) -> Option> { + ) -> Result>> { log::trace!("Handling streamed completion event: {:?}", event); use LanguageModelCompletionEvent::*; @@ -1310,7 +1356,7 @@ impl Thread { } RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), ToolUse(tool_use) => { - return self.handle_tool_use_event(tool_use, event_stream, cx); + return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); } ToolUseJsonParseError { id, @@ -1318,18 +1364,46 @@ impl Thread { raw_input, json_parse_error, } => { - return Some(Task::ready(self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, + return Ok(Some(Task::ready( + self.handle_tool_use_json_parse_error_event( + id, + tool_name, + raw_input, + json_parse_error, + ), ))); } - UsageUpdate(_) | StatusUpdate(_) => {} - Stop(_) => unreachable!(), + UsageUpdate(usage) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = self.id.to_string(), + prompt_id = self.prompt_id.to_string(), + model = self.model.as_ref().map(|m| m.telemetry_id()), + model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + self.update_token_usage(usage, cx); + } + StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { + self.update_model_request_usage(amount, limit, cx); + } + StatusUpdate( + CompletionRequestStatus::Started + | CompletionRequestStatus::Queued { .. } + | CompletionRequestStatus::Failed { .. }, + ) => {} + StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { + self.tool_use_limit_reached = true; + } + Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), + Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), + Stop(StopReason::ToolUse | StopReason::EndTurn) => {} } - None + Ok(None) } fn handle_text_event( @@ -1393,7 +1467,7 @@ impl Thread { ) -> Option> { cx.notify(); - let tool = self.tools.get(tool_use.name.as_ref()).cloned(); + let tool = self.tool(tool_use.name.as_ref()); let mut title = SharedString::from(&tool_use.name); let mut kind = acp::ToolKind::Other; if let Some(tool) = tool.as_ref() { @@ -1457,7 +1531,7 @@ impl Thread { }); let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::info!("Running tool {}", tool_use.name); + log::debug!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output @@ -1506,6 +1580,21 @@ impl Thread { } } + fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context) { + self.project + .read(cx) + .user_store() + .update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); + } + pub fn title(&self) -> SharedString { self.title.clone().unwrap_or("New Thread".into()) } @@ -1518,7 +1607,7 @@ impl Thread { return Task::ready(Err(anyhow!("No summarization model available"))); }; let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), + intent: Some(CompletionIntent::ThreadContextSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), ..Default::default() }; @@ -1540,12 +1629,11 @@ impl Thread { let text = match event { LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, + CompletionRequestStatus::UsageUpdated { amount, limit }, ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; continue; } _ => continue, @@ -1555,7 +1643,7 @@ impl Thread { summary.extend(lines.next()); } - log::info!("Setting summary: {}", summary); + log::debug!("Setting summary: {}", summary); let summary = SharedString::from(summary); this.update(cx, |this, cx| { @@ -1567,22 +1655,15 @@ impl Thread { }) } - fn update_title( - &mut self, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Option>> { - if self.title.is_some() { - log::debug!("Skipping title generation because we already have one."); - return None; - } + fn generate_title(&mut self, cx: &mut Context) { + let Some(model) = self.summarization_model.clone() else { + return; + }; - log::info!( + log::debug!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); - let model = self.summarization_model.clone()?; - let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), @@ -1598,44 +1679,53 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - Some(cx.spawn(async move |this, cx| { + self.pending_title_generation = Some(cx.spawn(async move |this, cx| { let mut title = String::new(); - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, - ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update - continue; + + let generate = async { + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + ) => { + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + title.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; } - _ => continue, - }; - - let mut lines = text.lines(); - title.extend(lines.next()); - - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; } + anyhow::Ok(()) + }; + + if generate.await.context("failed to generate title").is_ok() { + _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); } - - log::info!("Setting title: {}", title); - - this.update(cx, |this, cx| { - let title = SharedString::from(title); - event_stream.send_title_update(title.clone()); - this.title = Some(title); - cx.notify(); - }) - })) + _ = this.update(cx, |this, _| this.pending_title_generation = None); + })); } + + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { + self.pending_title_generation = None; + if Some(&title) != self.title.as_ref() { + self.title = Some(title); + cx.emit(TitleUpdated); + cx.notify(); + } + } + fn last_user_message(&self) -> Option<&UserMessage> { self.messages .iter() @@ -1656,6 +1746,10 @@ impl Thread { return; }; + if message.content.is_empty() { + return; + } + for content in &message.content { let AgentMessageContent::ToolUse(tool_use) = content else { continue; @@ -1684,34 +1778,32 @@ impl Thread { pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, - cx: &mut App, + cx: &App, ) -> Result { let model = self.model().context("No language model configured")?; + let tools = if let Some(turn) = self.running_turn.as_ref() { + turn.tools + .iter() + .filter_map(|(tool_name, tool)| { + log::trace!("Including tool: {}", tool_name); + Some(LanguageModelRequestTool { + name: tool_name.to_string(), + description: tool.description().to_string(), + input_schema: tool.input_schema(model.tool_input_format()).log_err()?, + }) + }) + .collect::>() + } else { + Vec::new() + }; log::debug!("Building completion request"); log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); let messages = self.build_request_messages(cx); - log::info!("Request will include {} messages", messages.len()); - - let tools = if let Some(tools) = self.tools(cx).log_err() { - tools - .filter_map(|tool| { - let tool_name = tool.name().to_string(); - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name, - description: tool.description().to_string(), - input_schema: tool.input_schema(model.tool_input_format()).log_err()?, - }) - }) - .collect() - } else { - Vec::new() - }; - - log::info!("Request includes {} tools", tools.len()); + log::debug!("Request will include {} messages", messages.len()); + log::debug!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { thread_id: Some(self.id.to_string()), @@ -1730,37 +1822,76 @@ impl Thread { Ok(request) } - fn tools<'a>(&'a self, cx: &'a App) -> Result>> { - let model = self.model().context("No language model configured")?; + fn enabled_tools( + &self, + profile: &AgentProfileSettings, + model: &Arc, + cx: &App, + ) -> BTreeMap> { + fn truncate(tool_name: &SharedString) -> SharedString { + if tool_name.len() > MAX_TOOL_NAME_LENGTH { + let mut truncated = tool_name.to_string(); + truncated.truncate(MAX_TOOL_NAME_LENGTH); + truncated.into() + } else { + tool_name.clone() + } + } - let profile = AgentSettings::get_global(cx) - .profiles - .get(&self.profile_id) - .context("profile not found")?; - let provider_id = model.provider_id(); - - Ok(self + let mut tools = self .tools .iter() - .filter(move |(_, tool)| tool.supported_provider(&provider_id)) .filter_map(|(tool_name, tool)| { - if profile.is_tool_enabled(tool_name) { - Some(tool) + if tool.supported_provider(&model.provider_id()) + && profile.is_tool_enabled(tool_name) + { + Some((truncate(tool_name), tool.clone())) } else { None } }) - .chain(self.context_server_registry.read(cx).servers().flat_map( - |(server_id, tools)| { - tools.iter().filter_map(|(tool_name, tool)| { - if profile.is_context_server_tool_enabled(&server_id.0, tool_name) { - Some(tool) - } else { - None - } - }) - }, - ))) + .collect::>(); + + let mut context_server_tools = Vec::new(); + let mut seen_tools = tools.keys().cloned().collect::>(); + let mut duplicate_tool_names = HashSet::default(); + for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { + for (tool_name, tool) in server_tools { + if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { + let tool_name = truncate(tool_name); + if !seen_tools.insert(tool_name.clone()) { + duplicate_tool_names.insert(tool_name.clone()); + } + context_server_tools.push((server_id.clone(), tool_name, tool.clone())); + } + } + } + + // When there are duplicate tool names, disambiguate by prefixing them + // with the server ID. In the rare case there isn't enough space for the + // disambiguated tool name, keep only the last tool with this name. + for (server_id, tool_name, tool) in context_server_tools { + if duplicate_tool_names.contains(&tool_name) { + let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); + if available >= 2 { + let mut disambiguated = server_id.0.to_string(); + disambiguated.truncate(available - 1); + disambiguated.push('_'); + disambiguated.push_str(&tool_name); + tools.insert(disambiguated.into(), tool.clone()); + } else { + tools.insert(tool_name, tool.clone()); + } + } else { + tools.insert(tool_name, tool.clone()); + } + } + + tools + } + + fn tool(&self, name: &str) -> Option> { + self.running_turn.as_ref()?.tools.get(name).cloned() } fn build_request_messages(&self, cx: &App) -> Vec { @@ -1768,21 +1899,29 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); - let mut messages = vec![self.build_system_message(cx)]; + + let system_prompt = SystemPromptTemplate { + project: self.project_context.read(cx), + available_tools: self.tools.keys().cloned().collect(), + } + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + let mut messages = vec![LanguageModelRequestMessage { + role: Role::System, + content: vec![system_prompt.into()], + cache: false, + }]; for message in &self.messages { messages.extend(message.to_request()); } - if let Some(message) = self.pending_message.as_ref() { - messages.extend(message.to_request()); + if let Some(last_message) = messages.last_mut() { + last_message.cache = true; } - if let Some(last_user_message) = messages - .iter_mut() - .rev() - .find(|message| message.role == Role::User) - { - last_user_message.cache = true; + if let Some(message) = self.pending_message.as_ref() { + messages.extend(message.to_request()); } messages @@ -1925,6 +2064,8 @@ struct RunningTurn { /// The current event stream for the running turn. Used to report a final /// cancellation event if we cancel the turn. event_stream: ThreadEventStream, + /// The tools that were enabled for this turn. + tools: BTreeMap>, } impl RunningTurn { @@ -1934,6 +2075,14 @@ impl RunningTurn { } } +pub struct TokenUsageUpdated(pub Option); + +impl EventEmitter for Thread {} + +pub struct TitleUpdated; + +impl EventEmitter for Thread {} + pub trait AgentTool where Self: 'static + Sized, @@ -1941,7 +2090,7 @@ where type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; type Output: for<'de> Deserialize<'de> + Serialize + Into; - fn name(&self) -> SharedString; + fn name() -> &'static str; fn description(&self) -> SharedString { let schema = schemars::schema_for!(Self::Input); @@ -1953,7 +2102,7 @@ where ) } - fn kind(&self) -> acp::ToolKind; + fn kind() -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. fn initial_title(&self, input: Result) -> SharedString; @@ -2029,7 +2178,7 @@ where T: AgentTool, { fn name(&self) -> SharedString { - self.0.name() + T::name().into() } fn description(&self) -> SharedString { @@ -2037,7 +2186,7 @@ where } fn kind(&self) -> agent_client_protocol::ToolKind { - self.0.kind() + T::kind() } fn initial_title(&self, input: serde_json::Value) -> SharedString { @@ -2091,12 +2240,6 @@ where struct ThreadEventStream(mpsc::UnboundedSender>); impl ThreadEventStream { - fn send_title_update(&self, text: SharedString) { - self.0 - .unbounded_send(Ok(ThreadEvent::TitleUpdate(text))) - .ok(); - } - fn send_user_message(&self, message: &UserMessage) { self.0 .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) @@ -2166,40 +2309,17 @@ impl ThreadEventStream { .ok(); } - fn send_token_usage_update(&self, usage: acp_thread::TokenUsage) { - self.0 - .unbounded_send(Ok(ThreadEvent::TokenUsageUpdate(usage))) - .ok(); - } - fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } - fn send_stop(&self, reason: StopReason) { - match reason { - StopReason::EndTurn => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::EndTurn))) - .ok(); - } - StopReason::MaxTokens => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::MaxTokens))) - .ok(); - } - StopReason::Refusal => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Refusal))) - .ok(); - } - StopReason::ToolUse => {} - } + fn send_stop(&self, reason: acp::StopReason) { + self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); } fn send_canceled(&self) { self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) .ok(); } @@ -2437,18 +2557,15 @@ impl From for acp::ContentBlock { uri: None, }), UserMessageContent::Mention { uri, content } => { - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: uri.to_uri().to_string(), - name: uri.name(), + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: content, + uri: uri.to_uri().to_string(), + }, + ), annotations: None, - description: if content.is_empty() { - None - } else { - Some(content) - }, - mime_type: None, - size: None, - title: None, }) } } diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index d1f2b3b1c7..bcca7eecd1 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -16,6 +16,29 @@ mod terminal_tool; mod thinking_tool; mod web_search_tool; +/// A list of all built in tool names, for use in deduplicating MCP tool names +pub fn default_tool_names() -> impl Iterator { + [ + CopyPathTool::name(), + CreateDirectoryTool::name(), + DeletePathTool::name(), + DiagnosticsTool::name(), + EditFileTool::name(), + FetchTool::name(), + FindPathTool::name(), + GrepTool::name(), + ListDirectoryTool::name(), + MovePathTool::name(), + NowTool::name(), + OpenTool::name(), + ReadFileTool::name(), + TerminalTool::name(), + ThinkingTool::name(), + WebSearchTool::name(), + ] + .into_iter() +} + pub use context_server_registry::*; pub use copy_path_tool::*; pub use create_directory_tool::*; @@ -33,3 +56,5 @@ pub use read_file_tool::*; pub use terminal_tool::*; pub use thinking_tool::*; pub use web_search_tool::*; + +use crate::AgentTool; diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index 69c4221a81..c7963fa6e6 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -176,15 +176,13 @@ impl AnyAgentTool for ContextServerTool { return Task::ready(Err(anyhow!("Context server not found"))); }; let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index f973b86990..819a6ff209 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -1,23 +1,18 @@ use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol::ToolKind; use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; +use gpui::{App, AppContext, Entity, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; use util::markdown::MarkdownInlineCode; -/// Copies a file or directory in the project, and returns confirmation that the -/// copy succeeded. -/// +/// Copies a file or directory in the project, and returns confirmation that the copy succeeded. /// Directory contents will be copied recursively (like `cp -r`). /// -/// This tool should be used when it's desirable to create a copy of a file or -/// directory without modifying the original. It's much more efficient than -/// doing this by separately reading and then writing the file or directory's -/// contents, so this tool should be preferred over that approach whenever -/// copying is the goal. +/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original. +/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CopyPathToolInput { /// The source path of the file or directory to copy. @@ -33,12 +28,10 @@ pub struct CopyPathToolInput { /// You can copy the first file by providing a source_path of "directory1/a/something.txt" /// pub source_path: String, - /// The destination path where the file or directory should be copied to. /// /// - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", - /// provide a destination_path of "directory2/b/copy.txt" + /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt" /// pub destination_path: String, } @@ -57,11 +50,11 @@ impl AgentTool for CopyPathTool { type Input = CopyPathToolInput; type Output = String; - fn name(&self) -> SharedString { - "copy_path".into() + fn name() -> &'static str { + "copy_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index c173c5ae67..652363d5fa 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode; use crate::{AgentTool, ToolCallEventStream}; -/// Creates a new directory at the specified path within the project. Returns -/// confirmation that the directory was created. +/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. /// -/// This tool creates a directory and all necessary parent directories (similar -/// to `mkdir -p`). It should be used whenever you need to create new -/// directories within the project. +/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreateDirectoryToolInput { /// The path of the new directory. @@ -44,11 +41,11 @@ impl AgentTool for CreateDirectoryTool { type Input = CreateDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "create_directory".into() + fn name() -> &'static str { + "create_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index e013b3a3e7..0f9641127f 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -9,8 +9,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -/// Deletes the file or directory (and the directory's contents, recursively) at -/// the specified path in the project, and returns confirmation of the deletion. +/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DeletePathToolInput { /// The path of the file or directory to delete. @@ -45,11 +44,11 @@ impl AgentTool for DeletePathTool { type Input = DeletePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "delete_path".into() + fn name() -> &'static str { + "delete_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Delete } diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index 6ba8b7b377..558bb918ce 100644 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for DiagnosticsTool { type Input = DiagnosticsToolInput; type Output = String; - fn name(&self) -> SharedString { - "diagnostics".into() + fn name() -> &'static str { + "diagnostics" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index a87699bd12..5a68d0c70a 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file"; /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be - /// shown in the UI and also passed to another model to perform the edit. + /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// - /// Be terse, but also descriptive in what you want to achieve with this - /// edit. Avoid generic instructions. + /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// Fix API endpoint URLs /// Update copyright year in `page_footer` /// - /// Make sure to include this field before all the others in the input object - /// so that we can display it immediately. + /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// - /// WARNING: When specifying which file path need changing, you MUST - /// start each path with one of the project's root directories. + /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend @@ -61,22 +57,19 @@ pub struct EditFileToolInput { /// /// `backend/src/main.rs` /// - /// Notice how the file path starts with `backend`. Without that, the path - /// would be ambiguous and the call would fail! + /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// /// /// /// `frontend/db.js` /// pub path: PathBuf, - /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// - /// When a file already exists or you just created it, prefer editing - /// it as opposed to recreating it from scratch. + /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: EditFileMode, } @@ -193,11 +186,11 @@ impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; - fn name(&self) -> SharedString { - "edit_file".into() + fn name() -> &'static str { + "edit_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Edit } @@ -427,7 +420,7 @@ impl AgentTool for EditFileTool { Ok(EditFileToolOutput { input_path: input.path, - new_text: new_text.clone(), + new_text, old_text, diff: unified_diff, edit_agent_output, @@ -524,7 +517,6 @@ fn resolve_path( mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; - use action_log::ActionLog; use client::TelemetrySettings; use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; @@ -542,7 +534,6 @@ mod tests { fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -551,7 +542,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log, Templates::new(), Some(model), cx, @@ -742,7 +732,6 @@ mod tests { } }); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -751,7 +740,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -808,7 +796,9 @@ mod tests { "Code should be formatted when format_on_save is enabled" ); - let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + let stale_buffer_count = thread + .read_with(cx, |thread, _cx| thread.action_log.clone()) + .read_with(cx, |log, cx| log.stale_buffers(cx).count()); assert_eq!( stale_buffer_count, 0, @@ -886,14 +876,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1015,14 +1003,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1153,14 +1139,12 @@ mod tests { let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1261,7 +1245,6 @@ mod tests { ) .await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1270,7 +1253,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1343,7 +1325,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1352,7 +1333,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1428,7 +1408,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1437,7 +1416,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1510,7 +1488,6 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1519,7 +1496,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index ae26c5fe19..dd97271a79 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -118,11 +118,11 @@ impl AgentTool for FetchTool { type Input = FetchToolInput; type Output = String; - fn name(&self) -> SharedString { - "fetch".into() + fn name() -> &'static str { + "fetch" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } @@ -136,12 +136,17 @@ impl AgentTool for FetchTool { fn run( self: Arc, input: Self::Input, - _event_stream: ToolCallEventStream, + event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { + let authorize = event_stream.authorize(input.url.clone(), cx); + let text = cx.background_spawn({ let http_client = self.http_client.clone(); - async move { Self::build_message(http_client, &input.url).await } + async move { + authorize.await?; + Self::build_message(http_client, &input.url).await + } }); cx.foreground_executor().spawn(async move { diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 552de144a7..384bd56e77 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -31,7 +31,6 @@ pub struct FindPathToolInput { /// You can get back the first two paths by providing a glob of "*thing*.txt" /// pub glob: String, - /// Optional starting position for paginated results (0-based). /// When not provided, starts from the beginning. #[serde(default)] @@ -86,11 +85,11 @@ impl AgentTool for FindPathTool { type Input = FindPathToolInput; type Output = FindPathToolOutput; - fn name(&self) -> SharedString { - "find_path".into() + fn name() -> &'static str { + "find_path" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } @@ -116,7 +115,7 @@ impl AgentTool for FindPathTool { ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(if paginated_matches.len() == 0 { + title: Some(if paginated_matches.is_empty() { "No matches".into() } else if paginated_matches.len() == 1 { "1 match".into() @@ -166,16 +165,17 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task SharedString { - "grep".into() + fn name() -> &'static str { + "grep" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } @@ -318,7 +317,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -403,7 +402,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -478,7 +477,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( @@ -763,7 +762,7 @@ mod tests { if cfg!(windows) { result.replace("root\\", "root/") } else { - result.to_string() + result } } Err(e) => panic!("Failed to run grep tool: {}", e), diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 61f21d8f95..e6fa8d7431 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -10,14 +10,12 @@ use std::fmt::Write; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Lists files and directories in a given path. Prefer the `grep` or -/// `find_path` tools when searching the codebase. +/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListDirectoryToolInput { /// The fully-qualified path of the directory to list in the project. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// /// If the project has the following root directories: @@ -53,11 +51,11 @@ impl AgentTool for ListDirectoryTool { type Input = ListDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "list_directory".into() + fn name() -> &'static str { + "list_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index f8d5d0d176..d9fb60651b 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Moves or rename a file or directory in the project, and returns confirmation -/// that the move succeeded. +/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. /// -/// If the source and destination directories are the same, but the filename is -/// different, this performs a rename. Otherwise, it performs a move. +/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move. /// -/// This tool should be used when it's desirable to move or rename a file or -/// directory without changing its contents at all. +/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct MovePathToolInput { /// The source path of the file or directory to move/rename. @@ -55,11 +52,11 @@ impl AgentTool for MovePathTool { type Input = MovePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "move_path".into() + fn name() -> &'static str { + "move_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index a72ede26fe..9467e7db68 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -32,11 +32,11 @@ impl AgentTool for NowTool { type Input = NowToolInput; type Output = String; - fn name(&self) -> SharedString { - "now".into() + fn name() -> &'static str { + "now" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index 36420560c1..df7b04c787 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; use util::markdown::MarkdownEscaped; -/// This tool opens a file or URL with the default application associated with -/// it on the user's operating system: +/// This tool opens a file or URL with the default application associated with it on the user's operating system: /// /// - On macOS, it's equivalent to the `open` command /// - On Windows, it's equivalent to `start` /// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate /// -/// For example, it can open a web browser with a URL, open a PDF file with the -/// default PDF viewer, etc. +/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc. /// -/// You MUST ONLY use this tool when the user has explicitly requested opening -/// something. You MUST NEVER assume that the user would like for you to use -/// this tool. +/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct OpenToolInput { /// The path or URL to open with the default application. @@ -41,11 +37,11 @@ impl AgentTool for OpenTool { type Input = OpenToolInput; type Output = String; - fn name(&self) -> SharedString { - "open".into() + fn name() -> &'static str { + "open" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Execute } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index f37dff4f47..e771c26eca 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -10,7 +10,8 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; +use util::markdown::MarkdownCodeBlock; use crate::{AgentTool, ToolCallEventStream}; @@ -21,8 +22,7 @@ use crate::{AgentTool, ToolCallEventStream}; pub struct ReadFileToolInput { /// The relative path of the file to read. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// /// If the project has the following root directories: @@ -34,11 +34,9 @@ pub struct ReadFileToolInput { /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. /// pub path: String, - /// Optional line number to start reading on (1-based index) #[serde(default)] pub start_line: Option, - /// Optional line number to end reading on (1-based index, inclusive) #[serde(default)] pub end_line: Option, @@ -62,36 +60,21 @@ impl AgentTool for ReadFileTool { type Input = ReadFileToolInput; type Output = LanguageModelToolResultContent; - fn name(&self) -> SharedString { - "read_file".into() + fn name() -> &'static str { + "read_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - let path = &input.path; - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!( - "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", - path, start, end, path, start, end - ) - } - (Some(start), None) => { - format!( - "[Read file `{}` (from line {})](@selection:{}:({}-{}))", - path, start, path, start, start - ) - } - _ => format!("[Read file `{}`](@file:{})", path, path), - } - .into() - } else { - "Read file".into() - } + input + .ok() + .as_ref() + .and_then(|input| Path::new(&input.path).file_name()) + .map(|file_name| file_name.to_string_lossy().to_string().into()) + .unwrap_or_default() } fn run( @@ -261,6 +244,19 @@ impl AgentTool for ReadFileTool { }]), ..Default::default() }); + if let Ok(LanguageModelToolResultContent::Text(text)) = &result { + let markdown = MarkdownCodeBlock { + tag: &input.path, + text, + } + .to_string(); + event_stream.update_fields(ToolCallUpdateFields { + content: Some(vec![acp::ToolCallContent::Content { + content: markdown.into(), + }]), + ..Default::default() + }) + } } })?; diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 17e671fba3..f41b909d0b 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for TerminalTool { type Input = TerminalToolInput; type Output = String; - fn name(&self) -> SharedString { - "terminal".into() + fn name() -> &'static str { + "terminal" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Execute } @@ -234,7 +234,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index 43647bb468..61fb9eb0d6 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream}; /// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ThinkingToolInput { - /// Content to think about. This should be a description of what to think about or - /// a problem to solve. + /// Content to think about. This should be a description of what to think about or a problem to solve. content: String, } @@ -22,11 +21,11 @@ impl AgentTool for ThinkingTool { type Input = ThinkingToolInput; type Output = String; - fn name(&self) -> SharedString { - "thinking".into() + fn name() -> &'static str { + "thinking" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Think } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index d71a128bfe..d7a34bec29 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -14,7 +14,7 @@ use ui::prelude::*; use web_search::WebSearchRegistry; /// Search the web for information using your query. -/// Use this when you need real-time information, facts, or data that might not be in your training. \ +/// Use this when you need real-time information, facts, or data that might not be in your training. /// Results will include snippets and links from relevant web pages. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct WebSearchToolInput { @@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool { type Input = WebSearchToolInput; type Output = WebSearchToolOutput; - fn name(&self) -> SharedString { - "web_search".into() + fn name() -> &'static str { + "web_search" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index b654486cb6..9f90f3a78a 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] e2e = [] [lints] @@ -17,16 +17,20 @@ path = "src/agent_servers.rs" doctest = false [dependencies] +acp_tools.workspace = true acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true -agentic-coding-protocol.workspace = true anyhow.workspace = true +client = { workspace = true, optional = true } collections.workspace = true context_server.workspace = true +env_logger = { workspace = true, optional = true } +fs = { workspace = true, optional = true } futures.workspace = true gpui.workspace = true +gpui_tokio = { workspace = true, optional = true } indoc.workspace = true itertools.workspace = true language.workspace = true @@ -36,6 +40,7 @@ log.workspace = true paths.workspace = true project.workspace = true rand.workspace = true +reqwest_client = { workspace = true, optional = true } schemars.workspace = true semver.workspace = true serde.workspace = true @@ -57,8 +62,12 @@ libc.workspace = true nix.workspace = true [dev-dependencies] +client = { workspace = true, features = ["test-support"] } env_logger.workspace = true +fs.workspace = true language.workspace = true indoc.workspace = true acp_thread = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +gpui_tokio.workspace = true +reqwest_client = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 1cfb1fcabf..9080fc1ab0 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -1,34 +1,392 @@ -use std::{path::Path, rc::Rc}; - use crate::AgentServerCommand; use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::AsyncApp; +use acp_tools::AcpConnectionRegistry; +use action_log::ActionLog; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use anyhow::anyhow; +use collections::HashMap; +use futures::AsyncBufReadExt as _; +use futures::channel::oneshot; +use futures::io::BufReader; +use project::Project; +use serde::Deserialize; +use std::{any::Any, cell::RefCell}; +use std::{path::Path, rc::Rc}; use thiserror::Error; -mod v0; -mod v1; +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; + +use acp_thread::{AcpThread, AuthRequired, LoadError}; #[derive(Debug, Error)] #[error("Unsupported version")] pub struct UnsupportedVersion; +pub struct AcpConnection { + server_name: SharedString, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, + _io_task: Task>, +} + +pub struct AcpSession { + thread: WeakEntity, + suppress_abort_err: bool, +} + pub async fn connect( - server_name: &'static str, + server_name: SharedString, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; + let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?; + Ok(Rc::new(conn) as _) +} - match conn { - Ok(conn) => Ok(Rc::new(conn) as _), - Err(err) if err.is::() => { - // Consider re-using initialize response and subprocess when adding another version here - let conn: Rc = - Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); - Ok(conn) +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: SharedString, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; + log::trace!("Spawned (pid: {})", child.id()); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); + + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name.clone(), &connection, cx) + }); + })?; + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); } - Err(err) => Err(err), + + Ok(Self { + auth_methods: response.auth_methods, + connection, + server_name, + sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) + } else { + anyhow!(err) + } + })?; + + let session_id = response.session_id; + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|cx| { + AcpThread::new( + self.server_name.clone(), + self.clone(), + project, + action_log, + session_id.clone(), + // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. + watch::Receiver::constant(self.prompt_capabilities), + cx, + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + suppress_abort_err: false, + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); + cx.foreground_executor().spawn(async move { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err + && (details.contains("This operation was aborted") + || details.contains("The user aborted a request")) + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } + }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; + } + let conn = self.connection.clone(); + let params = acp::CancelNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancel(params).await }) + .detach(); + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx?.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) } } diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 30643dd005..be96048929 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -498,6 +498,14 @@ impl AgentConnection for AcpConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: false, + audio: false, + embedded_context: false, + } + } + fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { let task = self .connection diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index e0e92f29ba..1945ad2483 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,11 +1,13 @@ +use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _}; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use anyhow::anyhow; use collections::HashMap; use futures::AsyncBufReadExt as _; use futures::channel::oneshot; use futures::io::BufReader; use project::Project; +use serde::Deserialize; use std::path::Path; use std::rc::Rc; use std::{any::Any, cell::RefCell}; @@ -21,11 +23,13 @@ pub struct AcpConnection { connection: Rc, sessions: Rc>>, auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, _io_task: Task>, } pub struct AcpSession { thread: WeakEntity, + suppress_abort_err: bool, } const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; @@ -98,6 +102,14 @@ impl AcpConnection { }) .detach(); + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + let response = connection .initialize(acp::InitializeRequest { protocol_version: acp::VERSION, @@ -116,9 +128,10 @@ impl AcpConnection { Ok(Self { auth_methods: response.auth_methods, - connection: connection.into(), + connection, server_name, sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, _io_task: io_task, }) } @@ -169,6 +182,7 @@ impl AgentConnection for AcpConnection { let session = AcpSession { thread: thread.downgrade(), + suppress_abort_err: false, }; sessions.borrow_mut().insert(session_id, session); @@ -200,13 +214,64 @@ impl AgentConnection for AcpConnection { cx: &mut App, ) -> Task> { let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - let response = conn.prompt(params).await?; - Ok(response) + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; + } let conn = self.connection.clone(); let params = acp::CancelNotification { session_id: session_id.clone(), @@ -246,7 +311,7 @@ impl acp::Client for ClientDelegate { let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, }; Ok(acp::RequestPermissionResponse { outcome }) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index cebf82cddb..7c7e124ca7 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,12 +1,14 @@ mod acp; mod claude; +mod custom; mod gemini; mod settings; -#[cfg(test)] -mod e2e_tests; +#[cfg(any(test, feature = "test-support"))] +pub mod e2e_tests; pub use claude::*; +pub use custom::*; pub use gemini::*; pub use settings::*; @@ -31,9 +33,10 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> &'static str; - fn empty_state_headline(&self) -> &'static str; - fn empty_state_message(&self) -> &'static str; + fn name(&self) -> SharedString; + fn empty_state_headline(&self) -> SharedString; + fn empty_state_message(&self) -> SharedString; + fn telemetry_id(&self) -> &'static str; fn connect( &self, @@ -95,7 +98,7 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub(crate) async fn resolve( + pub async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], fallback_path: Option<&Path>, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 3008edebeb..250e564526 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -30,7 +30,7 @@ use futures::{ io::BufReader, select_biased, }; -use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::{ResultExt, debug_panic}; @@ -43,16 +43,20 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn name(&self) -> &'static str { - "Claude Code" + fn telemetry_id(&self) -> &'static str { + "claude-code" } - fn empty_state_headline(&self) -> &'static str { + fn name(&self) -> SharedString { + "Claude Code".into() + } + + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + fn empty_state_message(&self) -> SharedString { + "How can I help you today?".into() } fn logo(&self) -> ui::IconName { @@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection { }); let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Claude Code", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + }), + cx, ) })?; @@ -697,7 +707,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) @@ -787,7 +797,7 @@ impl Content { pub fn chunks(self) -> impl Iterator { match self { Self::Chunks(chunks) => chunks.into_iter(), - Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(), + Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(), } } } @@ -1085,7 +1095,7 @@ pub(crate) mod tests { use gpui::TestAppContext; use serde_json::json; - crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); + crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow"); pub fn local_command() -> AgentServerCommand { AgentServerCommand { @@ -1117,7 +1127,7 @@ pub(crate) mod tests { thread.read_with(cx, |thread, _| { entries_len = thread.plan().entries.len(); - assert!(thread.plan().entries.len() > 0, "Empty plan"); + assert!(!thread.plan().entries.is_empty(), "Empty plan"); }); thread diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 3be10ed94c..3231903001 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -58,7 +58,7 @@ impl ClaudeTool { Self::Terminal(None) } else { Self::Other { - name: tool_name.to_string(), + name: tool_name, input, } } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs new file mode 100644 index 0000000000..72823026d7 --- /dev/null +++ b/crates/agent_servers/src/custom.rs @@ -0,0 +1,63 @@ +use crate::{AgentServerCommand, AgentServerSettings}; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::{App, Entity, SharedString, Task}; +use project::Project; +use std::{path::Path, rc::Rc}; +use ui::IconName; + +/// A generic agent server implementation for custom user-defined agents +pub struct CustomAgentServer { + name: SharedString, + command: AgentServerCommand, +} + +impl CustomAgentServer { + pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { + Self { + name, + command: settings.command.clone(), + } + } +} + +impl crate::AgentServer for CustomAgentServer { + fn telemetry_id(&self) -> &'static str { + "custom" + } + + fn name(&self) -> SharedString { + self.name.clone() + } + + fn logo(&self) -> IconName { + IconName::Terminal + } + + fn empty_state_headline(&self) -> SharedString { + "No conversations yet".into() + } + + fn empty_state_message(&self) -> SharedString { + format!("Start a conversation with {}", self.name).into() + } + + fn connect( + &self, + root_dir: &Path, + _project: &Entity, + cx: &mut App, + ) -> Task>> { + let server_name = self.name(); + let command = self.command.clone(); + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |mut cx| { + crate::acp::connect(server_name, command, &root_dir, &mut cx).await + }) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 8b2703575d..42264b4b4f 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,24 +1,31 @@ +use crate::AgentServer; +use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; +use agent_client_protocol as acp; +use futures::{FutureExt, StreamExt, channel::mpsc, select}; +use gpui::{AppContext, Entity, TestAppContext}; +use indoc::indoc; +use project::{FakeFs, Project}; use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration, }; - -use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; -use agent_client_protocol as acp; - -use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{Entity, TestAppContext}; -use indoc::indoc; -use project::{FakeFs, Project}; -use settings::{Settings, SettingsStore}; use util::path; -pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_basic(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -42,8 +49,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont }); } -pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_path_mentions(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); std::fs::write( @@ -56,7 +67,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes ) .expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + tempdir.path(), + cx, + ) + .await; thread .update(cx, |thread, cx| { thread.send( @@ -110,15 +127,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes drop(tempdir); } -pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_tool_call(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); let foo_path = tempdir.path().join("foo"); std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| { @@ -152,14 +179,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp drop(tempdir); } -pub async fn test_tool_call_with_permission( - server: impl AgentServer + 'static, +pub async fn test_tool_call_with_permission( + server: F, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, -) { - let fs = init_test(cx).await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +) where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -247,11 +283,21 @@ pub async fn test_tool_call_with_permission( }); } -pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; +pub async fn test_cancel(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let _ = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -316,10 +362,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon }); } -pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_thread_drop(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) @@ -386,27 +442,42 @@ macro_rules! common_e2e_tests { } }; } +pub use common_e2e_tests; // Helpers pub async fn init_test(cx: &mut TestAppContext) -> Arc { + #[cfg(test)] + use settings::Settings; + env_logger::try_init().ok(); cx.update(|cx| { - let settings_store = SettingsStore::test(cx); + let settings_store = settings::SettingsStore::test(cx); cx.set_global(settings_store); Project::init_settings(cx); language::init(cx); + gpui_tokio::init(cx); + let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + client::init_settings(cx); + let client = client::Client::production(cx); + let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store, client, cx); + agent_settings::init(cx); crate::settings::init(cx); + #[cfg(test)] crate::AllAgentServersSettings::override_global( - AllAgentServersSettings { - claude: Some(AgentServerSettings { + crate::AllAgentServersSettings { + claude: Some(crate::AgentServerSettings { command: crate::claude::tests::local_command(), }), - gemini: Some(AgentServerSettings { + gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), + custom: collections::HashMap::default(), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 813f8b1fe0..5d6a70fa64 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,10 +4,10 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; +use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; -use ui::App; use crate::AllAgentServersSettings; @@ -17,16 +17,20 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn name(&self) -> &'static str { - "Gemini" + fn telemetry_id(&self) -> &'static str { + "gemini-cli" } - fn empty_state_headline(&self) -> &'static str { - "Welcome to Gemini" + fn name(&self) -> SharedString { + "Gemini CLI".into() } - fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands" + fn empty_state_headline(&self) -> SharedString { + self.name() + } + + fn empty_state_message(&self) -> SharedString { + "Ask questions, edit files, run commands".into() } fn logo(&self) -> ui::IconName { @@ -47,16 +51,20 @@ impl AgentServer for Gemini { settings.get::(None).gemini.clone() })?; - let Some(command) = + let Some(mut command) = AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: "npm install -g @google/gemini-cli@latest".into() + install_command: Self::install_command().into(), }.into()); }; + if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { + command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); + } + let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; if result.is_err() { let version_fut = util::command::new_smol_command(&command.path) @@ -84,7 +92,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@latest".into(), + upgrade_command: Self::upgrade_command().into(), }.into()) } } @@ -97,13 +105,27 @@ impl AgentServer for Gemini { } } +impl Gemini { + pub fn binary_name() -> &'static str { + "gemini" + } + + pub fn install_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } + + pub fn upgrade_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } +} + #[cfg(test)] pub(crate) mod tests { use super::*; use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); + crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 645674b5f1..96ac6e3cbe 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,6 +1,7 @@ use crate::AgentServerCommand; use anyhow::Result; -use gpui::App; +use collections::HashMap; +use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -13,9 +14,13 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + + /// Custom agent servers configured by the user + #[serde(flatten)] + pub custom: HashMap, } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { + for AllAgentServersSettings { + gemini, + claude, + custom, + } in sources.defaults_and_customizations() + { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } + + // Merge custom agents + for (name, config) in custom { + // Skip built-in agent names to avoid conflicts + if name != "gemini" && name != "claude" { + settings.custom.insert(name.clone(), config.clone()); + } + } } Ok(settings) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 1fe41d002c..ed1ed2b898 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -505,9 +505,8 @@ impl Settings for AgentSettings { } } - debug_assert_eq!( - sources.default.always_allow_tool_actions.unwrap_or(false), - false, + debug_assert!( + !sources.default.always_allow_tool_actions.unwrap_or(false), "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" ); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 43e3b25124..6b0979ee69 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -67,6 +67,7 @@ ordered-float.workspace = true parking_lot.workspace = true paths.workspace = true picker.workspace = true +postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 999e469d30..5b40967069 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,8 +1,11 @@ +use std::cell::Cell; use std::ops::Range; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent_client_protocol as acp; use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; @@ -63,6 +66,7 @@ pub struct ContextPickerCompletionProvider { workspace: WeakEntity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, } impl ContextPickerCompletionProvider { @@ -71,12 +75,14 @@ impl ContextPickerCompletionProvider { workspace: WeakEntity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, ) -> Self { Self { message_editor, workspace, history_store, prompt_store, + prompt_capabilities, } } @@ -89,7 +95,7 @@ impl ContextPickerCompletionProvider { ) -> Option { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -102,62 +108,7 @@ impl ContextPickerCompletionProvider { confirm: Some(Arc::new(|_, _, _| true)), }), ContextPickerEntry::Action(action) => { - let (new_text, on_action) = match action { - ContextPickerAction::AddSelections => { - const PLACEHOLDER: &str = "selection "; - let selections = selection_ranges(workspace, cx) - .into_iter() - .enumerate() - .map(|(ix, (buffer, range))| { - ( - buffer, - range, - (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), - ) - }) - .collect::>(); - - let new_text: String = PLACEHOLDER.repeat(selections.len()); - - let callback = Arc::new({ - let source_range = source_range.clone(); - move |_, window: &mut Window, cx: &mut App| { - let selections = selections.clone(); - let message_editor = message_editor.clone(); - let source_range = source_range.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_selection( - source_range, - selections, - window, - cx, - ) - }) - .ok(); - }); - false - } - }); - - (new_text, callback) - } - }; - - Some(Completion { - replace_range: source_range.clone(), - new_text, - label: CodeLabel::plain(action.label().to_string(), None), - icon_path: Some(action.icon().path().into()), - documentation: None, - source: project::CompletionSource::Custom, - insert_text_mode: None, - // This ensures that when a user accepts this completion, the - // completion menu will still be shown after "@category " is - // inserted - confirm: Some(on_action), - }) + Self::completion_for_action(action, source_range, message_editor, workspace, cx) } } } @@ -187,7 +138,7 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion.clone()), + icon_path: Some(icon_for_completion), confirm: Some(confirm_completion_callback( thread_entry.title().clone(), source_range.start, @@ -218,9 +169,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), confirm: Some(confirm_completion_callback( - rule.title.clone(), + rule.title, source_range.start, new_text_len - 1, editor, @@ -260,7 +211,7 @@ impl ContextPickerCompletionProvider { let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { - crease_icon_path.clone() + crease_icon_path }; let new_text = format!("{} ", uri.as_link()); @@ -296,9 +247,9 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let uri = MentionUri::Symbol { - path: abs_path, + abs_path, name: symbol.name.clone(), - line_range: symbol.range.start.0.row..symbol.range.end.0.row, + line_range: symbol.range.start.0.row..=symbol.range.end.0.row, }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); @@ -309,10 +260,10 @@ impl ContextPickerCompletionProvider { label, documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( - symbol.name.clone().into(), + symbol.name.into(), source_range.start, new_text_len - 1, message_editor, @@ -327,7 +278,7 @@ impl ContextPickerCompletionProvider { message_editor: WeakEntity, cx: &mut App, ) -> Option { - let new_text = format!("@fetch {} ", url_to_fetch.clone()); + let new_text = format!("@fetch {} ", url_to_fetch); let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) .ok()?; @@ -341,7 +292,7 @@ impl ContextPickerCompletionProvider { label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( url_to_fetch.to_string().into(), @@ -353,6 +304,71 @@ impl ContextPickerCompletionProvider { }) } + pub(crate) fn completion_for_action( + action: ContextPickerAction, + source_range: Range, + message_editor: WeakEntity, + workspace: &Entity, + cx: &mut App, + ) -> Option { + let (new_text, on_action) = match action { + ContextPickerAction::AddSelections => { + const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::>(); + + let new_text: String = PLACEHOLDER.repeat(selections.len()); + + let callback = Arc::new({ + let source_range = source_range.clone(); + move |_, window: &mut Window, cx: &mut App| { + let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); + }); + false + } + }); + + (new_text, callback) + } + }; + + Some(Completion { + replace_range: source_range, + new_text, + label: CodeLabel::plain(action.label().to_string(), None), + icon_path: Some(action.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(on_action), + }) + } + fn search( &self, mode: Option, @@ -365,8 +381,7 @@ impl ContextPickerCompletionProvider { }; match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -377,8 +392,7 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -389,12 +403,8 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Thread) => { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - &self.history_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, &self.history_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -415,7 +425,7 @@ impl ContextPickerCompletionProvider { Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = self.prompt_store.as_ref() { let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -448,7 +458,7 @@ impl ContextPickerCompletionProvider { let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = self.available_context_picker_entries(&workspace, cx); let entry_candidates = entries @@ -550,17 +560,19 @@ impl ContextPickerCompletionProvider { }), ); - const RECENT_COUNT: usize = 2; - let threads = self - .history_store - .read(cx) - .recently_opened_entries(cx) - .into_iter() - .filter(|thread| !mentions.contains(&thread.mention_uri())) - .take(RECENT_COUNT) - .collect::>(); + if self.prompt_capabilities.get().embedded_context { + const RECENT_COUNT: usize = 2; + let threads = self + .history_store + .read(cx) + .recently_opened_entries(cx) + .into_iter() + .filter(|thread| !mentions.contains(&thread.mention_uri())) + .take(RECENT_COUNT) + .collect::>(); - recent.extend(threads.into_iter().map(Match::RecentThread)); + recent.extend(threads.into_iter().map(Match::RecentThread)); + } recent } @@ -570,11 +582,17 @@ impl ContextPickerCompletionProvider { workspace: &Entity, cx: &mut App, ) -> Vec { - let mut entries = vec![ - ContextPickerEntry::Mode(ContextPickerMode::File), - ContextPickerEntry::Mode(ContextPickerMode::Symbol), - ContextPickerEntry::Mode(ContextPickerMode::Thread), - ]; + let embedded_context = self.prompt_capabilities.get().embedded_context; + let mut entries = if embedded_context { + vec![ + ContextPickerEntry::Mode(ContextPickerMode::File), + ContextPickerEntry::Mode(ContextPickerMode::Symbol), + ContextPickerEntry::Mode(ContextPickerMode::Thread), + ] + } else { + // File is always available, but we don't need a mode entry + vec![] + }; let has_selection = workspace .read(cx) @@ -589,11 +607,13 @@ impl ContextPickerCompletionProvider { )); } - if self.prompt_store.is_some() { - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); - } + if embedded_context { + if self.prompt_store.is_some() { + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); + } - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + } entries } @@ -631,7 +651,11 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); let line = lines.next()?; - MentionCompletion::try_parse(line, offset_to_line) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) }); let Some(state) = state else { return Task::ready(Ok(Vec::new())); @@ -751,12 +775,16 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); if let Some(line) = lines.next() { - MentionCompletion::try_parse(line, offset_to_line) - .map(|completion| { - completion.source_range.start <= offset_to_line + position.column as usize - && completion.source_range.end >= offset_to_line + position.column as usize - }) - .unwrap_or(false) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) + .map(|completion| { + completion.source_range.start <= offset_to_line + position.column as usize + && completion.source_range.end >= offset_to_line + position.column as usize + }) + .unwrap_or(false) } else { false } @@ -777,7 +805,7 @@ pub(crate) fn search_threads( history_store: &Entity, cx: &mut App, ) -> Task> { - let threads = history_store.read(cx).entries(cx); + let threads = history_store.read(cx).entries().collect(); if query.is_empty() { return Task::ready(threads); } @@ -847,7 +875,7 @@ struct MentionCompletion { } impl MentionCompletion { - fn try_parse(line: &str, offset_to_line: usize) -> Option { + fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option { let last_mention_start = line.rfind('@')?; if last_mention_start >= line.len() { return Some(Self::default()); @@ -871,7 +899,9 @@ impl MentionCompletion { if let Some(mode_text) = parts.next() { end += mode_text.len(); - if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { + if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() + && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File)) + { mode = Some(parsed_mode); } else { argument = Some(mode_text.to_string()); @@ -904,10 +934,10 @@ mod tests { #[test] fn test_mention_completion_parse() { - assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None); assert_eq!( - MentionCompletion::try_parse("Lorem @", 0), + MentionCompletion::try_parse(true, "Lorem @", 0), Some(MentionCompletion { source_range: 6..7, mode: None, @@ -916,7 +946,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file", 0), + MentionCompletion::try_parse(true, "Lorem @file", 0), Some(MentionCompletion { source_range: 6..11, mode: Some(ContextPickerMode::File), @@ -925,7 +955,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file ", 0), + MentionCompletion::try_parse(true, "Lorem @file ", 0), Some(MentionCompletion { source_range: 6..12, mode: Some(ContextPickerMode::File), @@ -934,7 +964,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -943,7 +973,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs ", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -952,7 +982,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -961,7 +991,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @main", 0), + MentionCompletion::try_parse(true, "Lorem @main", 0), Some(MentionCompletion { source_range: 6..11, mode: None, @@ -969,6 +999,28 @@ mod tests { }) ); - assert_eq!(MentionCompletion::try_parse("test@", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None); + + // Allowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(true, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: Some(ContextPickerMode::Symbol), + argument: Some("main".to_string()), + }) + ); + + // Disallowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(false, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: None, + argument: Some("main".to_string()), + }) + ); } } diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 67acbb8b5b..0e4080d689 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,6 +1,7 @@ -use std::ops::Range; +use std::{cell::Cell, ops::Range, rc::Rc}; use acp_thread::{AcpThread, AgentThreadEntry}; +use agent_client_protocol::{PromptCapabilities, ToolCallId}; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -26,6 +27,7 @@ pub struct EntryViewState { prompt_store: Option>, entries: Vec, prevent_slash_commands: bool, + prompt_capabilities: Rc>, } impl EntryViewState { @@ -34,6 +36,7 @@ impl EntryViewState { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, prevent_slash_commands: bool, ) -> Self { Self { @@ -43,6 +46,7 @@ impl EntryViewState { prompt_store, entries: Vec::new(), prevent_slash_commands, + prompt_capabilities, } } @@ -80,6 +84,7 @@ impl EntryViewState { self.project.clone(), self.history_store.clone(), self.prompt_store.clone(), + self.prompt_capabilities.clone(), "Edit message - @ to include context", self.prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -106,6 +111,7 @@ impl EntryViewState { } } AgentThreadEntry::ToolCall(tool_call) => { + let id = tool_call.id.clone(); let terminals = tool_call.terminals().cloned().collect::>(); let diffs = tool_call.diffs().cloned().collect::>(); @@ -121,21 +127,31 @@ impl EntryViewState { for terminal in terminals { views.entry(terminal.entity_id()).or_insert_with(|| { - create_terminal( + let element = create_terminal( self.workspace.clone(), self.project.clone(), terminal.clone(), window, cx, ) - .into_any() + .into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewTerminal(id.clone()), + }); + element }); } for diff in diffs { - views - .entry(diff.entity_id()) - .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); + views.entry(diff.entity_id()).or_insert_with(|| { + let element = create_editor_diff(diff.clone(), window, cx).into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewDiff(id.clone()), + }); + element + }); } } AgentThreadEntry::AssistantMessage(_) => { @@ -187,6 +203,8 @@ pub struct EntryViewEvent { } pub enum ViewEvent { + NewDiff(ToolCallId), + NewTerminal(ToolCallId), MessageEditorEvent(Entity, MessageEditorEvent), } @@ -389,6 +407,7 @@ mod tests { project.clone(), history_store, None, + Default::default(), false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index c87c824015..12ae893c31 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,41 +1,42 @@ use crate::{ acp::completion_provider::ContextPickerCompletionProvider, - context_picker::fetch_context_picker::fetch_url_content, + context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent2::HistoryStore; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; use futures::{ - FutureExt as _, TryFutureExt as _, - future::{Shared, join_all, try_join_all}, + FutureExt as _, + future::{Shared, join_all}, }; use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, - UnderlineStyle, WeakEntity, + Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, + EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, + Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{Project, ProjectPath, Worktree}; -use prompt_store::PromptStore; +use postage::stream::Stream as _; +use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; +use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::Settings; use std::{ cell::Cell, ffi::OsStr, - fmt::{Display, Write}, - ops::Range, + fmt::Write, + ops::{Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -44,13 +45,12 @@ use std::{ use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, - IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, - Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, - h_flex, px, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, + FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, + LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, + TextSize, TintColor, Toggleable, Window, div, h_flex, px, }; -use url::Url; -use util::ResultExt; +use util::{ResultExt, debug_panic}; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; @@ -64,6 +64,7 @@ pub struct MessageEditor { history_store: Entity, prompt_store: Option>, prevent_slash_commands: bool, + prompt_capabilities: Rc>, _subscriptions: Vec, _parse_slash_command_task: Task<()>, } @@ -73,6 +74,7 @@ pub enum MessageEditorEvent { Send, Cancel, Focus, + LostFocus, } impl EventEmitter for MessageEditor {} @@ -83,6 +85,7 @@ impl MessageEditor { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, placeholder: impl Into>, prevent_slash_commands: bool, mode: EditorMode, @@ -101,6 +104,7 @@ impl MessageEditor { workspace.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), ); let semantics_provider = Rc::new(SlashCommandSemanticsProvider { range: Cell::new(None), @@ -128,17 +132,21 @@ impl MessageEditor { editor }); - cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { + cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); + cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { + cx.emit(MessageEditorEvent::LostFocus) + }) + .detach(); let mut subscriptions = Vec::new(); - if prevent_slash_commands { - subscriptions.push(cx.subscribe_in(&editor, window, { - let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event { + subscriptions.push(cx.subscribe_in(&editor, window, { + let semantics_provider = semantics_provider.clone(); + move |this, editor, event, window, cx| { + if let EditorEvent::Edited { .. } = event { + if prevent_slash_commands { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -146,9 +154,12 @@ impl MessageEditor { cx, ); } + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); + this.mention_set.remove_invalid(snapshot); + cx.notify(); } - })); - } + } + })); Self { editor, @@ -158,11 +169,42 @@ impl MessageEditor { history_store, prompt_store, prevent_slash_commands, + prompt_capabilities, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } + pub fn insert_thread_summary( + &mut self, + thread: agent2::DbThreadMetadata, + window: &mut Window, + cx: &mut Context, + ) { + let start = self.editor.update(cx, |editor, cx| { + editor.set_text(format!("{}\n", thread.title), window, cx); + editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_before(Point::zero()) + .text_anchor + }); + + self.confirm_completion( + thread.title.clone(), + start, + thread.title.len(), + MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + window, + cx, + ) + .detach(); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity { &self.editor @@ -179,9 +221,9 @@ impl MessageEditor { pub fn mentions(&self) -> HashSet { self.mention_set - .uri_by_crease_id + .mentions .values() - .cloned() + .map(|(uri, _)| uri.clone()) .collect() } @@ -200,106 +242,178 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return Task::ready(()); }; - let Some(anchor) = snapshot + let Some(start_anchor) = snapshot .buffer_snapshot .anchor_in_excerpt(*excerpt_id, start) else { return Task::ready(()); }; + let end_anchor = snapshot + .buffer_snapshot + .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - if let MentionUri::File { abs_path, .. } = &mention_uri { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(abs_path, cx) - else { - return Task::ready(()); - }; - let image = cx - .spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx)) - .map_err(|e| e.to_string())? - .await - .map_err(|e| e.to_string())?; - image - .read_with(cx, |image, _cx| image.image.clone()) - .map_err(|e| e.to_string()) - }) - .shared(); - let Some(crease_id) = insert_crease_for_image( - *excerpt_id, - start, - content_len, - Some(abs_path.as_path().into()), - image.clone(), - self.editor.clone(), - window, - cx, - ) else { - return Task::ready(()); - }; - return self.confirm_mention_for_image( - crease_id, - anchor, - Some(abs_path.clone()), - image, - window, - cx, - ); - } - } - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - *excerpt_id, - start, - content_len, - crease_text.clone(), - mention_uri.icon_path(cx), - self.editor.clone(), - window, - cx, - ) else { + let crease = if let MentionUri::File { abs_path } = &mention_uri + && let Some(extension) = abs_path.extension() + && let Some(extension) = extension.to_str() + && Img::extensions().contains(&extension) + && !extension.contains("svg") + { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + log::error!("project path not found"); + return Task::ready(()); + }; + let image = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + let image = cx + .spawn(async move |_, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let image = image + .update(cx, |image, _| image.image.clone()) + .map_err(|e| e.to_string())?; + Ok(image) + }) + .shared(); + insert_crease_for_mention( + *excerpt_id, + start, + content_len, + mention_uri.name().into(), + IconName::Image.path().into(), + Some(image), + self.editor.clone(), + window, + cx, + ) + } else { + insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text, + mention_uri.icon_path(cx), + None, + self.editor.clone(), + window, + cx, + ) + }; + let Some((crease_id, tx)) = crease else { return Task::ready(()); }; - match mention_uri { - MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) + let task = match mention_uri.clone() { + MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx), + MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx), + MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx), + MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx), + MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx), + MentionUri::Symbol { + abs_path, + line_range, + .. + } => self.confirm_mention_for_symbol(abs_path, line_range, cx), + MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx), + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be included in completions"); + Task::ready(Err(anyhow!( + "pasted imaged URI should not be included in completions" + ))) } - MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) + MentionUri::Selection { .. } => { + // Handled elsewhere + debug_panic!("unexpected selection URI"); + Task::ready(Err(anyhow!("unexpected selection URI"))) } - MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) + }; + let task = cx + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) + .shared(); + self.mention_set + .mentions + .insert(crease_id, (mention_uri, task.clone())); + + // Notify the user if we failed to load the mentioned context + cx.spawn_in(window, async move |this, cx| { + let result = task.await.notify_async_err(cx); + drop(tx); + if result.is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([(start_anchor..end_anchor, "")], cx); + }); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); } - MentionUri::TextThread { path, name } => { - self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx) - } - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - Task::ready(()) + }) + } + + fn confirm_mention_for_file( + &mut self, + abs_path: PathBuf, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + return Task::ready(Err(anyhow!("This model does not support images yet"))); } + let task = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + return cx.spawn(async move |_, cx| { + let image = task.await?; + let image = image.update(cx, |image, _| image.image.clone())?; + let format = image.format; + let image = cx + .update(|cx| LanguageModelImage::from_image(image, cx))? + .await; + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err(anyhow!("Failed to convert image")) + } + }); } + + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| Mention::Text { + content: buffer.text(), + tracked_buffers: vec![cx.entity()], + })?; + anyhow::Ok(mention) + }) } fn confirm_mention_for_directory( &mut self, - crease_id: CreaseId, - anchor: Anchor, abs_path: PathBuf, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { + ) -> Task> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { let mut files = Vec::new(); @@ -314,24 +428,21 @@ impl MessageEditor { files } - let uri = MentionUri::Directory { - abs_path: abs_path.clone(), - }; let Some(project_path) = self .project .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project path not found"))); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project entry not found"))); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("worktree not found"))); }; let project = self.project.clone(); - let task = cx.spawn(async move |_, cx| { + cx.spawn(async move |_, cx| { let directory_path = entry.path.clone(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; @@ -361,106 +472,103 @@ impl MessageEditor { let rope = buffer .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) .log_err()?; - Some(rope) + Some((rope, buffer)) }); cx.background_spawn(async move { - let rope = rope_task.await?; - Some((rel_path, full_path, rope.to_string())) + let (rope, buffer) = rope_task.await?; + Some((rel_path, full_path, rope.to_string(), buffer)) }) })) })?; let contents = cx .background_spawn(async move { - let contents = descendants_future.await.into_iter().flatten(); - contents.collect() + let (contents, tracked_buffers) = descendants_future + .await + .into_iter() + .flatten() + .map(|(rel_path, full_path, rope, buffer)| { + ((rel_path, full_path, rope), buffer) + }) + .unzip(); + Mention::Text { + content: render_directory_contents(contents), + tracked_buffers, + } }) .await; anyhow::Ok(contents) - }); - let task = cx - .spawn(async move |_, _| { - task.await - .map(|contents| DirectoryContents(contents).to_string()) - .map_err(|e| e.to_string()) - }) - .shared(); - - self.mention_set - .directories - .insert(abs_path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.directories.remove(&abs_path); - }) - .ok(); - } }) } fn confirm_mention_for_fetch( &mut self, - crease_id: CreaseId, - anchor: Anchor, url: url::Url, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let Some(http_client) = self + ) -> Task> { + let http_client = match self .workspace - .update(cx, |workspace, _cx| workspace.client().http_client()) - .ok() - else { - return Task::ready(()); + .update(cx, |workspace, _| workspace.client().http_client()) + { + Ok(http_client) => http_client, + Err(e) => return Task::ready(Err(e)), }; - - let url_string = url.to_string(); - let fetch = cx - .background_executor() - .spawn(async move { - fetch_url_content(http_client, url_string) - .map_err(|e| e.to_string()) - .await + cx.background_executor().spawn(async move { + let content = fetch_url_content(http_client, url.to_string()).await?; + Ok(Mention::Text { + content, + tracked_buffers: Vec::new(), }) - .shared(); - self.mention_set - .add_fetch_result(url.clone(), fetch.clone()); + }) + } - cx.spawn_in(window, async move |this, cx| { - let fetch = fetch.await.notify_async_err(cx); - this.update(cx, |this, cx| { - if fetch.is_some() { - this.mention_set - .insert_uri(crease_id, MentionUri::Fetch { url }); - } else { - // Remove crease if we failed to fetch - this.editor.update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }); - this.mention_set.fetch_results.remove(&url); + fn confirm_mention_for_symbol( + &mut self, + abs_path: PathBuf, + line_range: RangeInclusive, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| { + let start = Point::new(*line_range.start(), 0).min(buffer.max_point()); + let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point()); + let content = buffer.text_for_range(start..end).collect(); + Mention::Text { + content, + tracked_buffers: vec![cx.entity()], } + })?; + anyhow::Ok(mention) + }) + } + + fn confirm_mention_for_rule( + &mut self, + id: PromptId, + cx: &mut Context, + ) -> Task> { + let Some(prompt_store) = self.prompt_store.clone() else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let prompt = prompt_store.read(cx).load(id, cx); + cx.spawn(async move |_, _| { + let prompt = prompt.await?; + Ok(Mention::Text { + content: prompt, + tracked_buffers: Vec::new(), }) - .ok(); }) } @@ -485,21 +593,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let path = buffer + let abs_path = buffer .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + .project_path(cx) + .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)); let snapshot = buffer.read(cx).snapshot(); + let text = snapshot + .text_for_range(selection_range.clone()) + .collect::(); let point_range = selection_range.to_point(&snapshot); - let line_range = point_range.start.row..point_range.end.row; + let line_range = point_range.start.row..=point_range.end.row; let uri = MentionUri::Selection { - path: path.clone(), + abs_path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), + selection_name(abs_path.as_deref(), &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -511,137 +622,75 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set - .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + self.mention_set.mentions.insert( + crease_id, + ( + uri, + Task::ready(Ok(Mention::Text { + content: text, + tracked_buffers: vec![buffer], + })) + .shared(), + ), + ); } } fn confirm_mention_for_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, id: acp::SessionId, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::Thread { - id: id.clone(), - name, - }; + ) -> Task> { let server = Rc::new(agent2::NativeAgentServer::new( self.project.read(cx).fs().clone(), self.history_store.clone(), )); let connection = server.connect(Path::new(""), &self.project, cx); - let load_summary = cx.spawn({ - let id = id.clone(); - async move |_, cx| { - let agent = connection.await?; - let agent = agent.downcast::().unwrap(); - let summary = agent - .0 - .update(cx, |agent, cx| agent.thread_summary(id, cx))? - .await?; - anyhow::Ok(summary) - } - }); - let task = cx - .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) - .shared(); - - self.mention_set.insert_thread(id.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.thread_summaries.remove(&id); - }) - .ok(); - } + cx.spawn(async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(Mention::Text { + content: summary.to_string(), + tracked_buffers: Vec::new(), + }) }) } fn confirm_mention_for_text_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, path: PathBuf, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::TextThread { - path: path.clone(), - name, - }; + ) -> Task> { let context = self.history_store.update(cx, |text_thread_store, cx| { text_thread_store.load_text_thread(path.as_path().into(), cx) }); - let task = cx - .spawn(async move |_, cx| { - let context = context.await.map_err(|e| e.to_string())?; - let xml = context - .update(cx, |context, cx| context.to_xml(cx)) - .map_err(|e| e.to_string())?; - Ok(xml) + cx.spawn(async move |_, cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + Ok(Mention::Text { + content: xml, + tracked_buffers: Vec::new(), }) - .shared(); - - self.mention_set - .insert_text_thread(path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.text_thread_summaries.remove(&path); - }) - .ok(); - } }) } pub fn contents( &self, - window: &mut Window, cx: &mut Context, - ) -> Task>> { - let contents = - self.mention_set - .contents(&self.project, self.prompt_store.as_ref(), window, cx); + ) -> Task, Vec>)>> { + let contents = self + .mention_set + .contents(&self.prompt_capabilities.get(), cx); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; cx.spawn(async move |_, cx| { let contents = contents.await?; + let mut all_tracked_buffers = Vec::new(); editor.update(cx, |editor, cx| { let mut ix = 0; @@ -650,12 +699,7 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - - let Some(mention) = contents.get(&crease_id) else { + let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; @@ -672,7 +716,11 @@ impl MessageEditor { chunks.push(chunk); } let chunk = match mention { - Mention::Text { uri, content } => { + Mention::Text { + content, + tracked_buffers, + } => { + all_tracked_buffers.extend(tracked_buffers.iter().cloned()); acp::ContentBlock::Resource(acp::EmbeddedResource { annotations: None, resource: acp::EmbeddedResourceResource::TextResourceContents( @@ -685,14 +733,33 @@ impl MessageEditor { }) } Mention::Image(mention_image) => { + let uri = match uri { + MentionUri::File { .. } => Some(uri.to_uri().to_string()), + MentionUri::PastedImage => None, + other => { + debug_panic!( + "unexpected mention uri for image: {:?}", + other + ); + None + } + }; acp::ContentBlock::Image(acp::ImageContent { annotations: None, data: mention_image.data.to_string(), mime_type: mention_image.format.mime_type().into(), - uri: mention_image - .abs_path - .as_ref() - .map(|path| format!("file://{}", path.display())), + uri, + }) + } + Mention::UriOnly => { + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: uri.name(), + uri: uri.to_uri().to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, }) } }; @@ -715,7 +782,7 @@ impl MessageEditor { } }); - chunks + (chunks, all_tracked_buffers) }) }) } @@ -723,7 +790,13 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases(self.mention_set.drain(), cx) + editor.remove_creases( + self.mention_set + .mentions + .drain() + .map(|(crease_id, _)| crease_id), + cx, + ) }); } @@ -739,6 +812,10 @@ impl MessageEditor { } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + if !self.prompt_capabilities.get().image { + return; + } + let images = cx .read_from_clipboard() .map(|item| { @@ -759,7 +836,7 @@ impl MessageEditor { } cx.stop_propagation(); - let replacement_text = "image"; + let replacement_text = MentionUri::PastedImage.as_link().to_string(); for image in images { let (excerpt_id, text_anchor, multibuffer_anchor) = self.editor.update(cx, |message_editor, cx| { @@ -782,24 +859,64 @@ impl MessageEditor { }); let content_len = replacement_text.len(); - let Some(anchor) = multibuffer_anchor else { - return; + let Some(start_anchor) = multibuffer_anchor else { + continue; }; - let task = Task::ready(Ok(Arc::new(image))).shared(); - let Some(crease_id) = insert_crease_for_image( + let end_anchor = self.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) + }); + let image = Arc::new(image); + let Some((crease_id, tx)) = insert_crease_for_mention( excerpt_id, text_anchor, content_len, - None.clone(), - task.clone(), + MentionUri::PastedImage.name().into(), + IconName::Image.path().into(), + Some(Task::ready(Ok(image.clone())).shared()), self.editor.clone(), window, cx, ) else { - return; + continue; }; - self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) - .detach(); + let task = cx + .spawn_in(window, { + async move |_, cx| { + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + drop(tx); + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err("Failed to convert image".into()) + } + } + }) + .shared(); + + self.mention_set + .mentions + .insert(crease_id, (MentionUri::PastedImage, task.clone())); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.edit([(start_anchor..end_anchor, "")], cx); + }); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); + } + }) + .detach(); } } @@ -862,6 +979,38 @@ impl MessageEditor { .detach(); } + pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let Some(completion) = ContextPickerCompletionProvider::completion_for_action( + ContextPickerAction::AddSelections, + anchor..anchor, + cx.weak_entity(), + &workspace, + cx, + ) else { + return; + }; + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm { + confirm(CompletionIntent::Complete, window, cx); + } + } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { self.editor.update(cx, |message_editor, cx| { message_editor.set_read_only(read_only); @@ -869,67 +1018,6 @@ impl MessageEditor { }) } - fn confirm_mention_for_image( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: Option, - image: Shared, String>>>, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let editor = self.editor.clone(); - let task = cx - .spawn_in(window, { - let abs_path = abs_path.clone(); - async move |_, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - Ok(MentionImage { - abs_path, - data: image.source, - format, - }) - } else { - Err("Failed to convert image".into()) - } - } - }) - .shared(); - - self.mention_set.insert_image(crease_id, task.clone()); - - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - if let Some(abs_path) = abs_path.clone() { - this.update(cx, |this, _cx| { - this.mention_set - .insert_uri(crease_id, MentionUri::File { abs_path }); - }) - .ok(); - } - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.images.remove(&crease_id); - }) - .ok(); - } - }) - } - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -947,7 +1035,6 @@ impl MessageEditor { let mut text = String::new(); let mut mentions = Vec::new(); - let mut images = Vec::new(); for chunk in message { match chunk { @@ -958,22 +1045,60 @@ impl MessageEditor { resource: acp::EmbeddedResourceResource::TextResourceContents(resource), .. }) => { + let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else { + continue; + }; + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push(( + start..end, + mention_uri, + Mention::Text { + content: resource.text, + tracked_buffers: Vec::new(), + }, + )); + } + acp::ContentBlock::ResourceLink(resource) => { if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri, resource.text)); + mentions.push((start..end, mention_uri, Mention::UriOnly)); } } - acp::ContentBlock::Image(content) => { + acp::ContentBlock::Image(acp::ImageContent { + uri, + data, + mime_type, + annotations: _, + }) => { + let mention_uri = if let Some(uri) = uri { + MentionUri::parse(&uri) + } else { + Ok(MentionUri::PastedImage) + }; + let Some(mention_uri) = mention_uri.log_err() else { + continue; + }; + let Some(format) = ImageFormat::from_mime_type(&mime_type) else { + log::error!("failed to parse MIME type for image: {mime_type:?}"); + continue; + }; let start = text.len(); - text.push_str("image"); + write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - images.push((start..end, content)); + mentions.push(( + start..end, + mention_uri, + Mention::Image(MentionImage { + data: data.into(), + format, + }), + )); } - acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) - | acp::ContentBlock::ResourceLink(_) => {} + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } } @@ -982,88 +1107,27 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri, text) in mentions { + for (range, mention_uri, mention) in mentions { let anchor = snapshot.anchor_before(range.start); - let crease_id = crate::context_picker::insert_crease_for_mention( + let Some((crease_id, tx)) = insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, mention_uri.name().into(), mention_uri.icon_path(cx), + None, self.editor.clone(), window, cx, - ); - - if let Some(crease_id) = crease_id { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - } - - match mention_uri { - MentionUri::Thread { id, .. } => { - self.mention_set - .insert_thread(id, Task::ready(Ok(text.into())).shared()); - } - MentionUri::TextThread { path, .. } => { - self.mention_set - .insert_text_thread(path, Task::ready(Ok(text)).shared()); - } - MentionUri::Fetch { url } => { - self.mention_set - .add_fetch_result(url, Task::ready(Ok(text)).shared()); - } - MentionUri::Directory { abs_path } => { - let task = Task::ready(Ok(text)).shared(); - self.mention_set.directories.insert(abs_path, task); - } - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => {} - } - } - for (range, content) in images { - let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { + ) else { continue; }; - let anchor = snapshot.anchor_before(range.start); - let abs_path = content - .uri - .as_ref() - .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); + drop(tx); - let name = content - .uri - .as_ref() - .and_then(|uri| { - uri.strip_prefix("file://") - .and_then(|path| Path::new(path).file_name()) - }) - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or("Image".to_owned()); - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - name.into(), - IconName::Image.path().into(), - self.editor.clone(), - window, - cx, + self.mention_set.mentions.insert( + crease_id, + (mention_uri.clone(), Task::ready(Ok(mention)).shared()), ); - let data: SharedString = content.data.to_string().into(); - - if let Some(crease_id) = crease_id { - self.mention_set.insert_image( - crease_id, - Task::ready(Ok(MentionImage { - abs_path, - data, - format, - })) - .shared(), - ); - } } cx.notify(); } @@ -1110,29 +1174,25 @@ impl MessageEditor { }) } + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } + #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); }); } - - #[cfg(test)] - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } } -struct DirectoryContents(Arc<[(Arc, PathBuf, String)]>); - -impl Display for DirectoryContents { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (_relative_path, full_path, content) in self.0.iter() { - let fence = codeblock_fence_for_path(Some(full_path), None); - write!(f, "\n{fence}\n{content}\n```")?; - } - Ok(()) +fn render_directory_contents(entries: Vec<(Arc, PathBuf, String)>) -> String { + let mut output = String::new(); + for (_relative_path, full_path, content) in entries { + let fence = codeblock_fence_for_path(Some(&full_path), None); + write!(output, "\n{fence}\n{content}\n```").unwrap(); } + output } impl Focusable for MessageEditor { @@ -1180,23 +1240,21 @@ impl Render for MessageEditor { } } -pub(crate) fn insert_crease_for_image( +pub(crate) fn insert_crease_for_mention( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, - abs_path: Option>, - image: Shared, String>>>, + crease_label: SharedString, + crease_icon: SharedString, + // abs_path: Option>, + image: Option, String>>>>, editor: Entity, window: &mut Window, cx: &mut App, -) -> Option { - let crease_label = abs_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().to_string().into()) - .unwrap_or(SharedString::from("Image")); +) -> Option<(CreaseId, postage::barrier::Sender)> { + let (tx, rx) = postage::barrier::channel(); - editor.update(cx, |editor, cx| { + let crease_id = editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; @@ -1205,7 +1263,15 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), + render: render_fold_icon_button( + crease_label, + crease_icon, + start..end, + rx, + image, + cx.weak_entity(), + cx, + ), merge_adjacent: false, ..Default::default() }; @@ -1222,64 +1288,112 @@ pub(crate) fn insert_crease_for_image( editor.fold_creases(vec![crease], false, window, cx); Some(ids[0]) - }) + })?; + + Some((crease_id, tx)) } -fn render_image_fold_icon_button( +fn render_fold_icon_button( label: SharedString, - image_task: Shared, String>>>, + icon: SharedString, + range: Range, + mut loading_finished: postage::barrier::Receiver, + image_task: Option, String>>>>, editor: WeakEntity, + cx: &mut App, ) -> Arc, &mut App) -> AnyElement> { - Arc::new({ - let image_task = image_task.clone(); - move |fold_id, fold_range, cx| { - let is_in_text_selection = editor - .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) - .unwrap_or_default(); - - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Image) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ), - ) - .hoverable_tooltip({ - let image_task = image_task.clone(); - move |_, cx| { - let image = image_task.peek().cloned().transpose().ok().flatten(); - let image_task = image_task.clone(); - cx.new::(|cx| ImageHover { - image, - _task: cx.spawn(async move |this, cx| { - if let Ok(image) = image_task.clone().await { - this.update(cx, |this, cx| { - if this.image.replace(image).is_none() { - cx.notify(); - } - }) - .ok(); - } - }), - }) - .into() - } - }) - .into_any_element() + let loading = cx.new(|cx| { + let loading = cx.spawn(async move |this, cx| { + loading_finished.recv().await; + this.update(cx, |this: &mut LoadingContext, cx| { + this.loading = None; + cx.notify(); + }) + .ok(); + }); + LoadingContext { + id: cx.entity_id(), + label, + icon, + range, + editor, + loading: Some(loading), + image: image_task.clone(), } - }) + }); + Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element()) +} + +struct LoadingContext { + id: EntityId, + label: SharedString, + icon: SharedString, + range: Range, + editor: WeakEntity, + loading: Option>, + image: Option, String>>>>, +} + +impl Render for LoadingContext { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_in_text_selection = self + .editor + .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx)) + .unwrap_or_default(); + ButtonLike::new(("loading-context", self.id)) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .when_some(self.image.clone(), |el, image_task| { + el.hoverable_tooltip(move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + }) + }) + .child( + h_flex() + .gap_1() + .child( + Icon::from_path(self.icon.clone()) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(self.label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ) + .map(|el| { + if self.loading.is_some() { + el.with_animation( + "loading-context-crease", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any() + } else { + el.into_any() + } + }), + ) + } } struct ImageHover { @@ -1297,246 +1411,63 @@ impl Render for ImageHover { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Mention { - Text { uri: MentionUri, content: String }, + Text { + content: String, + tracked_buffers: Vec>, + }, Image(MentionImage), + UriOnly, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } #[derive(Default)] pub struct MentionSet { - uri_by_crease_id: HashMap, - fetch_results: HashMap>>>, - images: HashMap>>>, - thread_summaries: HashMap>>>, - text_thread_summaries: HashMap>>>, - directories: HashMap>>>, + mentions: HashMap>>)>, } impl MentionSet { - pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { - self.uri_by_crease_id.insert(crease_id, uri); - } - - pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { - self.fetch_results.insert(url, content); - } - - pub fn insert_image( - &mut self, - crease_id: CreaseId, - task: Shared>>, - ) { - self.images.insert(crease_id, task); - } - - fn insert_thread( - &mut self, - id: acp::SessionId, - task: Shared>>, - ) { - self.thread_summaries.insert(id, task); - } - - fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { - self.text_thread_summaries.insert(path, task); - } - - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.thread_summaries.clear(); - self.text_thread_summaries.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - - pub fn contents( + fn contents( &self, - project: &Entity, - prompt_store: Option<&Entity>, - _window: &mut Window, + prompt_capabilities: &acp::PromptCapabilities, cx: &mut App, - ) -> Task>> { - let mut processed_image_creases = HashSet::default(); + ) -> Task>> { + if !prompt_capabilities.embedded_context { + let mentions = self + .mentions + .iter() + .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly))) + .collect(); - let mut contents = self - .uri_by_crease_id - .iter() - .map(|(&crease_id, uri)| { - match uri { - MentionUri::File { abs_path, .. } => { - let uri = uri.clone(); - let abs_path = abs_path.to_path_buf(); - - if let Some(task) = self.images.get(&crease_id).cloned() { - processed_image_creases.insert(crease_id); - return cx.spawn(async move |_| { - let image = task.await.map_err(|e| anyhow!("{e}"))?; - anyhow::Ok((crease_id, Mention::Image(image))) - }); - } - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Directory { abs_path } => { - let Some(content) = self.directories.get(abs_path).cloned() else { - return Task::ready(Err(anyhow!("missing directory load task"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), - }, - )) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Thread { id, .. } => { - let Some(content) = self.thread_summaries.get(id).cloned() else { - return Task::ready(Err(anyhow!("missing thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), - }, - )) - }) - } - MentionUri::TextThread { path, .. } => { - let Some(content) = self.text_thread_summaries.get(path).cloned() else { - return Task::ready(Err(anyhow!("missing text thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), - }, - )) - }) - } - MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = prompt_store else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let text_task = prompt_store.read(cx).load(*prompt_id, cx); - let uri = uri.clone(); - cx.spawn(async move |_| { - // TODO: report load errors instead of just logging - let text = text_task.await?; - anyhow::Ok((crease_id, Mention::Text { uri, content: text })) - }) - } - MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(url).cloned() else { - return Task::ready(Err(anyhow!("missing fetch result"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - }, - )) - }) - } - } - }) - .collect::>(); - - // Handle images that didn't have a mention URI (because they were added by the paste handler). - contents.extend(self.images.iter().filter_map(|(crease_id, image)| { - if processed_image_creases.contains(crease_id) { - return None; - } - let crease_id = *crease_id; - let image = image.clone(); - Some(cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), - )) - })) - })); + return Task::ready(Ok(mentions)); + } + let mentions = self.mentions.clone(); cx.spawn(async move |_cx| { - let contents = try_join_all(contents).await?.into_iter().collect(); - anyhow::Ok(contents) + let mut contents = HashMap::default(); + for (crease_id, (mention_uri, task)) in mentions { + contents.insert( + crease_id, + (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?), + ); + } + Ok(contents) }) } + + fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + self.mentions.remove(&crease_id); + } + } + } } struct SlashCommandSemanticsProvider { @@ -1549,7 +1480,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let snapshot = buffer.read(cx).snapshot(); let offset = position.to_offset(&snapshot); let (start, end) = self.range.get()?; @@ -1557,14 +1488,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(vec![project::Hover { + Some(Task::ready(Some(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }])) + }]))) } fn inline_values( @@ -1614,7 +1545,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { _position: text::Anchor, _kind: editor::GotoDefinitionKind, _cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { None } @@ -1678,8 +1609,9 @@ impl Addon for MessageEditorAddon { #[cfg(test)] mod tests { - use std::{ops::Range, path::Path, sync::Arc}; + use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc}; + use acp_thread::MentionUri; use agent_client_protocol as acp; use agent2::HistoryStore; use assistant_context::ContextStore; @@ -1723,6 +1655,7 @@ mod tests { project.clone(), history_store.clone(), None, + Default::default(), "Test", false, EditorMode::AutoHeight { @@ -1792,10 +1725,8 @@ mod tests { editor.backspace(&Default::default(), window, cx); }); - let content = message_editor - .update_in(cx, |message_editor, window, cx| { - message_editor.contents(window, cx) - }) + let (content, _) = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) .await .unwrap(); @@ -1821,7 +1752,7 @@ mod tests { impl Focusable for MessageEditorItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -1862,7 +1793,8 @@ mod tests { "six.txt": "6", "seven.txt": "7", "eight.txt": "8", - } + }, + "x.png": "", }), ) .await; @@ -1913,6 +1845,7 @@ mod tests { let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -1922,6 +1855,7 @@ mod tests { project.clone(), history_store.clone(), None, + prompt_capabilities.clone(), "Test", false, EditorMode::AutoHeight { @@ -1947,6 +1881,31 @@ mod tests { (message_editor, editor) }); + cx.simulate_input("Lorem @"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "Lorem @"); + assert!(editor.has_visible_completions_menu()); + + // Only files since we have default capabilities + assert_eq!( + current_completion_labels(editor), + &[ + "eight.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "five.txt dir/b/", + ] + ); + editor.set_text("", window, cx); + }); + + prompt_capabilities.set(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }); + cx.simulate_input("Lorem "); editor.update(&mut cx, |editor, cx| { @@ -2012,24 +1971,48 @@ mod tests { assert_eq!(fold_ranges(editor, cx).len(), 1); }); + let all_prompt_capabilities = acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }; + let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { + .update(&mut cx, |message_editor, cx| { message_editor .mention_set() - .contents(&project, None, window, cx) + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() .into_values() .collect::>(); - pretty_assertions::assert_eq!( - contents, - [Mention::Text { - content: "1".into(), - uri: url_one.parse().unwrap() - }] - ); + { + let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); + } + + let contents = message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&acp::PromptCapabilities::default(), cx) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + { + let [(uri, Mention::UriOnly)] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); + } cx.simulate_input(" "); @@ -2065,25 +2048,25 @@ mod tests { cx.run_until_parked(); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { + .update(&mut cx, |message_editor, cx| { message_editor .mention_set() - .contents(&project, None, window, cx) + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() .into_values() .collect::>(); - assert_eq!(contents.len(), 2); let url_eight = uri!("file:///dir/b/eight.txt"); - pretty_assertions::assert_eq!( - contents[1], - Mention::Text { - content: "8".to_string(), - uri: url_eight.parse().unwrap(), - } - ); + + { + let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "8"); + pretty_assertions::assert_eq!(uri, &url_eight.parse::().unwrap()); + } editor.update(&mut cx, |editor, cx| { assert_eq!( @@ -2175,33 +2158,111 @@ mod tests { }); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { + .update(&mut cx, |message_editor, cx| { message_editor .mention_set() - .contents(&project, None, window, cx) + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() .into_values() .collect::>(); - assert_eq!(contents.len(), 3); - pretty_assertions::assert_eq!( - contents[2], - Mention::Text { - content: "1".into(), - uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), - } - ); + { + let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!( + uri, + &format!("{url_one}?symbol=MySymbol#L1:1") + .parse::() + .unwrap() + ); + } cx.run_until_parked(); editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Try to mention an "image" file that will fail to load + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // Getting the message contents fails + message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .expect_err("Should fail to load x.png"); + + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Once more + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // This time don't immediately get the contents, just let the confirmed completion settle + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Now getting the contents succeeds, because the invalid mention was removed + let contents = message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .unwrap(); + assert_eq!(contents.len(), 3); } fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { @@ -2219,7 +2280,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::>() } } diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 4dfa5e8d4f..967d977b82 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -1,19 +1,20 @@ -use crate::RemoveSelectedThread; +use crate::acp::AcpThreadView; +use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy::StringMatchCandidate; use gpui::{ - App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, - UniformListScrollHandle, Window, uniform_list, + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, + UniformListScrollHandle, WeakEntity, Window, uniform_list, }; -use std::{fmt::Display, ops::Range, sync::Arc}; +use std::{fmt::Display, ops::Range}; +use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar, prelude::*, }; -use util::ResultExt; pub struct AcpThreadHistory { pub(crate) history_store: Entity, @@ -21,36 +22,36 @@ pub struct AcpThreadHistory { selected_index: usize, hovered_index: Option, search_editor: Entity, - all_entries: Arc>, - // When the search is empty, we display date separators between history entries - // This vector contains an enum of either a separator or an actual entry - separated_items: Vec, - // Maps entry indexes to list item indexes - separated_item_indexes: Vec, - _separated_items_task: Option>, - search_state: SearchState, - local_timezone: UtcOffset, - _subscriptions: Vec, -} + search_query: SharedString, -enum SearchState { - Empty, - Searching { - query: SharedString, - _task: Task<()>, - }, - Searched { - query: SharedString, - matches: Vec, - }, + visible_items: Vec, + + local_timezone: UtcOffset, + + _update_task: Task<()>, + _subscriptions: Vec, } enum ListItemType { BucketSeparator(TimeBucket), Entry { - index: usize, + entry: HistoryEntry, format: EntryTimeFormat, }, + SearchResult { + entry: HistoryEntry, + positions: Vec, + }, +} + +impl ListItemType { + fn history_entry(&self) -> Option<&HistoryEntry> { + match self { + ListItemType::Entry { entry, .. } => Some(entry), + ListItemType::SearchResult { entry, .. } => Some(entry), + _ => None, + } + } } pub enum ThreadHistoryEvent { @@ -75,12 +76,15 @@ impl AcpThreadHistory { cx.subscribe(&search_editor, |this, search_editor, event, cx| { if let EditorEvent::BufferEdited = event { let query = search_editor.read(cx).text(cx); - this.search(query.into(), cx); + if this.search_query != query { + this.search_query = query.into(); + this.update_visible_items(false, cx); + } } }); let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_all_entries(cx); + this.update_visible_items(true, cx); }); let scroll_handle = UniformListScrollHandle::default(); @@ -90,38 +94,67 @@ impl AcpThreadHistory { scroll_handle, selected_index: 0, hovered_index: None, - search_state: SearchState::Empty, - all_entries: Default::default(), - separated_items: Default::default(), - separated_item_indexes: Default::default(), + visible_items: Default::default(), search_editor, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), + search_query: SharedString::default(), _subscriptions: vec![search_editor_subscription, history_store_subscription], - _separated_items_task: None, + _update_task: Task::ready(()), }; - this.update_all_entries(cx); + this.update_visible_items(false, cx); this } - fn update_all_entries(&mut self, cx: &mut Context) { - let new_entries: Arc> = self + fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { + let entries = self .history_store - .update(cx, |store, cx| store.entries(cx)) - .into(); + .update(cx, |store, _| store.entries().collect()); + let new_list_items = if self.search_query.is_empty() { + self.add_list_separators(entries, cx) + } else { + self.filter_search_results(entries, cx) + }; + let selected_history_entry = if preserve_selected_item { + self.selected_history_entry().cloned() + } else { + None + }; - self._separated_items_task.take(); + self._update_task = cx.spawn(async move |this, cx| { + let new_visible_items = new_list_items.await; + this.update(cx, |this, cx| { + let new_selected_index = if let Some(history_entry) = selected_history_entry { + let history_entry_id = history_entry.id(); + new_visible_items + .iter() + .position(|visible_entry| { + visible_entry + .history_entry() + .is_some_and(|entry| entry.id() == history_entry_id) + }) + .unwrap_or(0) + } else { + 0 + }; - let mut items = Vec::with_capacity(new_entries.len() + 1); - let mut indexes = Vec::with_capacity(new_entries.len() + 1); + this.visible_items = new_visible_items; + this.set_selected_index(new_selected_index, Bias::Right, cx); + cx.notify(); + }) + .ok(); + }); + } - let bg_task = cx.background_spawn(async move { + fn add_list_separators(&self, entries: Vec, cx: &App) -> Task> { + cx.background_spawn(async move { + let mut items = Vec::with_capacity(entries.len() + 1); let mut bucket = None; let today = Local::now().naive_local().date(); - for (index, entry) in new_entries.iter().enumerate() { + for entry in entries.into_iter() { let entry_date = entry .updated_at() .with_timezone(&Local) @@ -134,75 +167,33 @@ impl AcpThreadHistory { items.push(ListItemType::BucketSeparator(entry_bucket)); } - indexes.push(items.len() as u32); items.push(ListItemType::Entry { - index, + entry, format: entry_bucket.into(), }); } - (new_entries, items, indexes) - }); - - let task = cx.spawn(async move |this, cx| { - let (new_entries, items, indexes) = bg_task.await; - this.update(cx, |this, cx| { - let previously_selected_entry = - this.all_entries.get(this.selected_index).map(|e| e.id()); - - this.all_entries = new_entries; - this.separated_items = items; - this.separated_item_indexes = indexes; - - match &this.search_state { - SearchState::Empty => { - if this.selected_index >= this.all_entries.len() { - this.set_selected_entry_index( - this.all_entries.len().saturating_sub(1), - cx, - ); - } else if let Some(prev_id) = previously_selected_entry - && let Some(new_ix) = this - .all_entries - .iter() - .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } - } - SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { - this.search(query.clone(), cx); - } - } - - cx.notify(); - }) - .log_err(); - }); - self._separated_items_task = Some(task); + items + }) } - fn search(&mut self, query: SharedString, cx: &mut Context) { - if query.is_empty() { - self.search_state = SearchState::Empty; - cx.notify(); - return; - } - - let all_entries = self.all_entries.clone(); - - let fuzzy_search_task = cx.background_spawn({ - let query = query.clone(); + fn filter_search_results( + &self, + entries: Vec, + cx: &App, + ) -> Task> { + let query = self.search_query.clone(); + cx.background_spawn({ let executor = cx.background_executor().clone(); async move { - let mut candidates = Vec::with_capacity(all_entries.len()); + let mut candidates = Vec::with_capacity(entries.len()); - for (idx, entry) in all_entries.iter().enumerate() { + for (idx, entry) in entries.iter().enumerate() { candidates.push(StringMatchCandidate::new(idx, entry.title())); } const MAX_MATCHES: usize = 100; - fuzzy::match_strings( + let matches = fuzzy::match_strings( &candidates, &query, false, @@ -211,74 +202,61 @@ impl AcpThreadHistory { &Default::default(), executor, ) - .await + .await; + + matches + .into_iter() + .map(|search_match| ListItemType::SearchResult { + entry: entries[search_match.candidate_id].clone(), + positions: search_match.positions, + }) + .collect() } - }); - - let task = cx.spawn({ - let query = query.clone(); - async move |this, cx| { - let matches = fuzzy_search_task.await; - - this.update(cx, |this, cx| { - let SearchState::Searching { - query: current_query, - _task, - } = &this.search_state - else { - return; - }; - - if &query == current_query { - this.search_state = SearchState::Searched { - query: query.clone(), - matches, - }; - - this.set_selected_entry_index(0, cx); - cx.notify(); - }; - }) - .log_err(); - } - }); - - self.search_state = SearchState::Searching { query, _task: task }; - cx.notify(); - } - - fn matched_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.all_entries.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } - } - - fn list_item_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.separated_items.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } + }) } fn search_produced_no_matches(&self) -> bool { - match &self.search_state { - SearchState::Empty => false, - SearchState::Searching { .. } => false, - SearchState::Searched { matches, .. } => matches.is_empty(), - } + self.visible_items.is_empty() && !self.search_query.is_empty() } - fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { - match &self.search_state { - SearchState::Empty => self.all_entries.get(ix), - SearchState::Searching { .. } => None, - SearchState::Searched { matches, .. } => matches - .get(ix) - .and_then(|m| self.all_entries.get(m.candidate_id)), + fn selected_history_entry(&self) -> Option<&HistoryEntry> { + self.get_history_entry(self.selected_index) + } + + fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> { + self.visible_items.get(visible_items_ix)?.history_entry() + } + + fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context) { + if self.visible_items.len() == 0 { + self.selected_index = 0; + return; } + while matches!( + self.visible_items.get(index), + None | Some(ListItemType::BucketSeparator(..)) + ) { + index = match bias { + Bias::Left => { + if index == 0 { + self.visible_items.len() - 1 + } else { + index - 1 + } + } + Bias::Right => { + if index >= self.visible_items.len() - 1 { + 0 + } else { + index + 1 + } + } + }; + } + self.selected_index = index; + self.scroll_handle + .scroll_to_item(index, ScrollStrategy::Top); + cx.notify() } pub fn select_previous( @@ -287,13 +265,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == 0 { - self.set_selected_entry_index(count - 1, cx); - } else { - self.set_selected_entry_index(self.selected_index - 1, cx); - } + if self.selected_index == 0 { + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + } else { + self.set_selected_index(self.selected_index - 1, Bias::Left, cx); } } @@ -303,13 +278,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == count - 1 { - self.set_selected_entry_index(0, cx); - } else { - self.set_selected_entry_index(self.selected_index + 1, cx); - } + if self.selected_index == self.visible_items.len() - 1 { + self.set_selected_index(0, Bias::Right, cx); + } else { + self.set_selected_index(self.selected_index + 1, Bias::Right, cx); } } @@ -319,35 +291,11 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(0, cx); - } + self.set_selected_index(0, Bias::Right, cx); } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(count - 1, cx); - } - } - - fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context) { - self.selected_index = entry_index; - - let scroll_ix = match self.search_state { - SearchState::Empty | SearchState::Searching { .. } => self - .separated_item_indexes - .get(entry_index) - .map(|ix| *ix as usize) - .unwrap_or(entry_index + 1), - SearchState::Searched { .. } => entry_index, - }; - - self.scroll_handle - .scroll_to_item(scroll_ix, ScrollStrategy::Top); - - cx.notify(); + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); } fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { @@ -355,7 +303,7 @@ impl AcpThreadHistory { } fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { + let Some(entry) = self.get_history_entry(ix) else { return; }; cx.emit(ThreadHistoryEvent::Open(entry.clone())); @@ -370,8 +318,8 @@ impl AcpThreadHistory { self.remove_thread(self.selected_index, cx) } - fn remove_thread(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { + fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(visible_item_ix) else { return; }; @@ -386,55 +334,33 @@ impl AcpThreadHistory { task.detach_and_log_err(cx); } - fn list_items( + fn render_list_items( &mut self, range: Range, _window: &mut Window, cx: &mut Context, ) -> Vec { - match &self.search_state { - SearchState::Empty => self - .separated_items - .get(range) - .iter() - .flat_map(|items| { - items - .iter() - .map(|item| self.render_list_item(item, vec![], cx)) - }) - .collect(), - SearchState::Searched { matches, .. } => matches[range] - .iter() - .filter_map(|m| { - let entry = self.all_entries.get(m.candidate_id)?; - Some(self.render_history_entry( - entry, - EntryTimeFormat::DateAndTime, - m.candidate_id, - m.positions.clone(), - cx, - )) - }) - .collect(), - SearchState::Searching { .. } => { - vec![] - } - } + self.visible_items + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) + .collect() } - fn render_list_item( - &self, - item: &ListItemType, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { + fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { match item { - ListItemType::Entry { index, format } => match self.all_entries.get(*index) { - Some(entry) => self - .render_history_entry(entry, *format, *index, highlight_positions, cx) - .into_any(), - None => Empty.into_any_element(), - }, + ListItemType::Entry { entry, format } => self + .render_history_entry(entry, *format, ix, Vec::default(), cx) + .into_any(), + ListItemType::SearchResult { entry, positions } => self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + ix, + positions.clone(), + cx, + ), ListItemType::BucketSeparator(bucket) => div() .px(DynamicSpacing::Base06.rems(cx)) .pt_2() @@ -452,12 +378,12 @@ impl AcpThreadHistory { &self, entry: &HistoryEntry, format: EntryTimeFormat, - list_entry_ix: usize, + ix: usize, highlight_positions: Vec, cx: &Context, ) -> AnyElement { - let selected = list_entry_ix == self.selected_index; - let hovered = Some(list_entry_ix) == self.hovered_index; + let selected = ix == self.selected_index; + let hovered = Some(ix) == self.hovered_index; let timestamp = entry.updated_at().timestamp(); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); @@ -465,7 +391,7 @@ impl AcpThreadHistory { .w_full() .pb_1() .child( - ListItem::new(list_entry_ix) + ListItem::new(ix) .rounded() .toggle_state(selected) .spacing(ListItemSpacing::Sparse) @@ -487,14 +413,14 @@ impl AcpThreadHistory { ) .on_hover(cx.listener(move |this, is_hovered, _window, cx| { if *is_hovered { - this.hovered_index = Some(list_entry_ix); - } else if this.hovered_index == Some(list_entry_ix) { + this.hovered_index = Some(ix); + } else if this.hovered_index == Some(ix) { this.hovered_index = None; } cx.notify(); })) - .end_slot::(if hovered || selected { + .end_slot::(if hovered { Some( IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) @@ -503,16 +429,14 @@ impl AcpThreadHistory { .tooltip(move |window, cx| { Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(list_entry_ix, cx) - })), + .on_click( + cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)), + ), ) } else { None }) - .on_click( - cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), - ), + .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), ) .into_any_element() } @@ -535,7 +459,7 @@ impl Render for AcpThreadHistory { .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::remove_selected_thread)) - .when(!self.all_entries.is_empty(), |parent| { + .when(!self.history_store.read(cx).is_empty(cx), |parent| { parent.child( h_flex() .h(px(41.)) // Match the toolbar perfectly @@ -561,7 +485,7 @@ impl Render for AcpThreadHistory { .overflow_hidden() .flex_grow(); - if self.all_entries.is_empty() { + if self.history_store.read(cx).is_empty(cx) { view.justify_center() .child( h_flex().w_full().justify_center().child( @@ -579,21 +503,160 @@ impl Render for AcpThreadHistory { view.pr_5().child( uniform_list( "thread-history", - self.list_item_count(), + self.visible_items.len(), cx.processor(|this, range: Range, window, cx| { - this.list_items(range, window, cx) + this.render_list_items(range, window, cx) }), ) .p_1() .track_scroll(self.scroll_handle.clone()) - .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) - .flex_grow(), + .flex_grow() + .vertical_scrollbar_for( + self.scroll_handle.clone(), + window, + cx, + ), ) } }) } } +#[derive(IntoElement)] +pub struct AcpHistoryEntryElement { + entry: HistoryEntry, + thread_view: WeakEntity, + selected: bool, + hovered: bool, + on_hover: Box, +} + +impl AcpHistoryEntryElement { + pub fn new(entry: HistoryEntry, thread_view: WeakEntity) -> Self { + Self { + entry, + thread_view, + selected: false, + hovered: false, + on_hover: Box::new(|_, _, _| {}), + } + } + + pub fn hovered(mut self, hovered: bool) -> Self { + self.hovered = hovered; + self + } + + pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_hover = Box::new(on_hover); + self + } +} + +impl RenderOnce for AcpHistoryEntryElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let id = self.entry.id(); + let title = self.entry.title(); + let timestamp = self.entry.updated_at(); + + let formatted_time = { + let now = chrono::Utc::now(); + let duration = now.signed_duration_since(timestamp); + + if duration.num_days() > 0 { + format!("{}d", duration.num_days()) + } else if duration.num_hours() > 0 { + format!("{}h ago", duration.num_hours()) + } else if duration.num_minutes() > 0 { + format!("{}m ago", duration.num_minutes()) + } else { + "Just now".to_string() + } + }; + + ListItem::new(id) + .rounded() + .toggle_state(self.selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Label::new(title).size(LabelSize::Small).truncate()) + .child( + Label::new(formatted_time) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(self.on_hover) + .end_slot::(if self.hovered || self.selected { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry.clone(); + + move |_event, _window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.update(cx, |thread_view, cx| { + thread_view.delete_history_entry(entry.clone(), cx); + }); + } + } + }), + ) + } else { + None + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry; + + move |_event, window, cx| { + if let Some(workspace) = thread_view + .upgrade() + .and_then(|view| view.read(cx).workspace().upgrade()) + { + match &entry { + HistoryEntry::AcpThread(thread_metadata) => { + if let Some(panel) = workspace.read(cx).panel::(cx) { + panel.update(cx, |panel, cx| { + panel.load_agent_thread( + thread_metadata.clone(), + window, + cx, + ); + }); + } + } + HistoryEntry::TextThread(context) => { + if let Some(panel) = workspace.read(cx).panel::(cx) { + panel.update(cx, |panel, cx| { + panel + .open_saved_prompt_editor( + context.path.clone(), + window, + cx, + ) + .detach_and_log_err(cx); + }); + } + } + } + } + } + }) + } +} + #[derive(Clone, Copy)] pub enum EntryTimeFormat { DateAndTime, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2f85ed416e..213020b05f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,17 +5,17 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore}; +use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ @@ -34,6 +34,8 @@ use project::{Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::cell::Cell; +use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -41,7 +43,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Tooltip, WithScrollbar, prelude::*, + SpinnerLabel, Tooltip, WithScrollbar, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -54,25 +56,36 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; -use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; + +use crate::ui::preview::UsageCallout; +use crate::ui::{ + AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, +}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector, + KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ThreadFeedback { + Positive, + Negative, +} + enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), ToolUseLimitReached, + AuthenticationRequired(SharedString), Other(SharedString), } impl ThreadError { - fn from_err(error: anyhow::Error) -> Self { + fn from_err(error: anyhow::Error, agent: &Rc) -> Self { if error.is::() { Self::PaymentRequired } else if error.is::() { @@ -82,7 +95,17 @@ impl ThreadError { { Self::ModelRequestLimitReached(error.plan) } else { - Self::Other(error.to_string().into()) + let string = error.to_string(); + // TODO: we should have Gemini return better errors here. + if agent.clone().downcast::().is_some() + && string.contains("Could not load the default credentials") + || string.contains("API key not valid") + || string.contains("Request had invalid authentication credentials") + { + Self::AuthenticationRequired(string.into()) + } else { + Self::Other(error.to_string().into()) + } } } } @@ -105,20 +128,145 @@ impl ProfileProvider for Entity { } } +#[derive(Default)] +struct ThreadFeedbackState { + feedback: Option, + comments_editor: Option>, +} + +impl ThreadFeedbackState { + pub fn submit( + &mut self, + thread: Entity, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut App, + ) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + if self.feedback == Some(feedback) { + return; + } + + self.feedback = Some(feedback); + match feedback { + ThreadFeedback::Positive => { + self.comments_editor = None; + } + ThreadFeedback::Negative => { + self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); + } + } + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + let rating = match feedback { + ThreadFeedback::Positive => "positive", + ThreadFeedback::Negative => "negative", + }; + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Rated", + session_id = session_id, + rating = rating, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn submit_comments(&mut self, thread: Entity, cx: &mut App) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + let Some(comments) = self + .comments_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + .filter(|text| !text.trim().is_empty()) + else { + return; + }; + + self.comments_editor.take(); + + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Feedback Comments", + session_id = session_id, + comments = comments, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn clear(&mut self) { + *self = Self::default() + } + + pub fn dismiss_comments(&mut self) { + self.comments_editor.take(); + } + + fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity { + let buffer = cx.new(|cx| { + let empty_string = String::new(); + MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) + }); + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: Some(4), + }, + buffer, + None, + window, + cx, + ); + editor.set_placeholder_text( + "What went wrong? Share your feedback so we can improve.", + cx, + ); + editor + }); + + editor.read(cx).focus_handle(cx).focus(window); + editor + } +} + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, thread_state: ThreadState, history_store: Entity, + hovered_recent_history_item: Option, entry_view_state: Entity, message_editor: Entity, + focus_handle: FocusHandle, model_selector: Option>, profile_selector: Option>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, thread_retry_status: Option, thread_error: Option, + thread_feedback: ThreadFeedbackState, list_state: ListState, auth_task: Option>, expanded_tool_calls: HashSet, @@ -126,8 +274,10 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - terminal_expanded: bool, + should_be_following: bool, editing_message: Option, + prompt_capabilities: Rc>, + is_loading_contents: bool, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -138,13 +288,15 @@ enum ThreadState { }, Ready { thread: Entity, - _subscription: [Subscription; 2], + title_editor: Option>, + _subscriptions: Vec, }, LoadError(LoadError), Unauthenticated { connection: Rc, description: Option>, configuration_view: Option, + pending_auth_method: Option, _subscription: Option, }, } @@ -153,6 +305,7 @@ impl AcpThreadView { pub fn new( agent: Rc, resume_thread: Option, + summarize_thread: Option, workspace: WeakEntity, project: Entity, history_store: Entity, @@ -160,14 +313,23 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Self { + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); + + let placeholder = if agent.name() == "Zed Agent" { + format!("Message the {} — @ to include context", agent.name()) + } else { + format!("Message {} — @ to include context", agent.name()) + }; + let message_editor = cx.new(|cx| { - MessageEditor::new( + let mut editor = MessageEditor::new( workspace.clone(), project.clone(), history_store.clone(), prompt_store.clone(), - "Message the agent — @ to include context", + prompt_capabilities.clone(), + placeholder, prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -175,7 +337,11 @@ impl AcpThreadView { }, window, cx, - ) + ); + if let Some(entry) = summarize_thread { + editor.insert_thread_summary(entry, window, cx); + } + editor }); let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); @@ -186,6 +352,7 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), prevent_slash_commands, ) }); @@ -210,6 +377,7 @@ impl AcpThreadView { list_state: list_state, thread_retry_status: None, thread_error: None, + thread_feedback: Default::default(), auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), @@ -217,10 +385,14 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, - terminal_expanded: true, + should_be_following: false, history_store, + hovered_recent_history_item: None, + prompt_capabilities, + is_loading_contents: false, _subscriptions: subscriptions, _cancel_task: None, + focus_handle: cx.focus_handle(), } } @@ -244,8 +416,12 @@ impl AcpThreadView { let connection = match connect_task.await { Ok(connection) => connection, Err(err) => { - this.update(cx, |this, cx| { - this.handle_load_error(err, cx); + this.update_in(cx, |this, window, cx| { + if err.downcast_ref::().is_some() { + this.handle_load_error(err, window, cx); + } else { + this.handle_thread_error(err, cx); + } cx.notify(); }) .log_err(); @@ -294,12 +470,10 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { match result { Ok(thread) => { - let thread_subscription = - cx.subscribe_in(&thread, window, Self::handle_thread_event); - let action_log = thread.read(cx).action_log().clone(); - let action_log_subscription = - cx.observe(&action_log, |_, _, cx| cx.notify()); + + this.prompt_capabilities + .set(thread.read(cx).prompt_capabilities()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -338,10 +512,33 @@ impl AcpThreadView { }) }); + let mut subscriptions = vec![ + cx.subscribe_in(&thread, window, Self::handle_thread_event), + cx.observe(&action_log, |_, _, cx| cx.notify()), + ]; + + let title_editor = + if thread.update(cx, |thread, cx| thread.can_set_title(cx)) { + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(thread.read(cx).title(), window, cx); + editor + }); + subscriptions.push(cx.subscribe_in( + &editor, + window, + Self::handle_title_editor_event, + )); + Some(editor) + } else { + None + }; this.thread_state = ThreadState::Ready { thread, - _subscription: [thread_subscription, action_log_subscription], + title_editor, + _subscriptions: subscriptions, }; + this.message_editor.focus_handle(cx).focus(window); this.profile_selector = this.as_native_thread(cx).map(|thread| { cx.new(|cx| { @@ -357,7 +554,7 @@ impl AcpThreadView { cx.notify(); } Err(err) => { - this.handle_load_error(err, cx); + this.handle_load_error(err, window, cx); } }; }) @@ -404,7 +601,7 @@ impl AcpThreadView { let view = registry.read(cx).provider(&provider_id).map(|provider| { provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name), + language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), window, cx, ) @@ -417,6 +614,7 @@ impl AcpThreadView { this.update(cx, |this, cx| { this.thread_state = ThreadState::Unauthenticated { + pending_auth_method: None, connection, configuration_view, description: err @@ -425,20 +623,35 @@ impl AcpThreadView { .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), _subscription: subscription, }; + if this.message_editor.focus_handle(cx).is_focused(window) { + this.focus_handle.focus(window) + } cx.notify(); }) .ok(); } - fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { + fn handle_load_error( + &mut self, + err: anyhow::Error, + window: &mut Window, + cx: &mut Context, + ) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); } else { self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into())) } + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } cx.notify(); } + pub fn workspace(&self) -> &WeakEntity { + &self.workspace + } + pub fn thread(&self) -> Option<&Entity> { match &self.thread_state { ThreadState::Ready { thread, .. } => Some(thread), @@ -448,12 +661,19 @@ impl AcpThreadView { } } - pub fn title(&self, cx: &App) -> SharedString { + pub fn title(&self) -> SharedString { match &self.thread_state { - ThreadState::Ready { thread, .. } => thread.read(cx).title(), + ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), - ThreadState::Unauthenticated { .. } => "Authentication Required".into(), + } + } + + pub fn title_editor(&self) -> Option> { + if let ThreadState::Ready { title_editor, .. } = &self.thread_state { + title_editor.clone() + } else { + None } } @@ -501,6 +721,35 @@ impl AcpThreadView { cx.notify(); } + pub fn handle_title_editor_event( + &mut self, + title_editor: &Entity, + event: &EditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread() else { return }; + + match event { + EditorEvent::BufferEdited => { + let new_title = title_editor.read(cx).text(cx); + thread.update(cx, |thread, cx| { + thread + .set_title(new_title.into(), cx) + .detach_and_log_err(cx); + }) + } + EditorEvent::Blurred => { + if title_editor.read(cx).text(cx).is_empty() { + title_editor.update(cx, |editor, cx| { + editor.set_text("New Thread", window, cx); + }); + } + } + _ => {} + } + } + pub fn handle_message_editor_event( &mut self, _: &Entity, @@ -514,6 +763,7 @@ impl AcpThreadView { MessageEditorEvent::Focus => { self.cancel_editing(&Default::default(), window, cx); } + MessageEditorEvent::LostFocus => {} } } @@ -525,9 +775,37 @@ impl AcpThreadView { cx: &mut Context, ) { match &event.view_event { + ViewEvent::NewDiff(tool_call_id) => { + if AgentSettings::get_global(cx).expand_edit_card { + self.expanded_tool_calls.insert(tool_call_id.clone()); + } + } + ViewEvent::NewTerminal(tool_call_id) => { + if AgentSettings::get_global(cx).expand_terminal_card { + self.expanded_tool_calls.insert(tool_call_id.clone()); + } + } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { - self.editing_message = Some(event.entry_index); - cx.notify(); + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + self.editing_message = Some(event.entry_index); + cx.notify(); + } + } + ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { + self.editing_message = None; + cx.notify(); + } + } } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); @@ -543,6 +821,9 @@ impl AcpThreadView { let Some(thread) = self.thread() else { return; }; + if !thread.read(cx).can_resume(cx) { + return; + } let task = thread.update(cx, |thread, cx| thread.resume(cx)); cx.spawn(async move |this, cx| { @@ -559,6 +840,11 @@ impl AcpThreadView { fn send(&mut self, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return }; + + if self.is_loading_contents { + return; + } + self.history_store.update(cx, |history, cx| { history.push_recently_opened_entry( HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), @@ -573,7 +859,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); self.send_impl(contents, window, cx) } @@ -586,7 +872,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); cx.spawn_in(window, async move |this, cx| { cancelled.await; @@ -601,18 +887,37 @@ impl AcpThreadView { fn send_impl( &mut self, - contents: Task>>, + contents: Task, Vec>)>>, window: &mut Window, cx: &mut Context, ) { + let agent_telemetry_id = self.agent.telemetry_id(); + self.thread_error.take(); self.editing_message.take(); + self.thread_feedback.clear(); let Some(thread) = self.thread().cloned() else { return; }; + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } + + self.is_loading_contents = true; + let guard = cx.new(|_| ()); + cx.observe_release(&guard, |this, _guard, cx| { + this.is_loading_contents = false; + cx.notify(); + }) + .detach(); + let task = cx.spawn_in(window, async move |this, cx| { - let contents = contents.await?; + let (contents, tracked_buffers) = contents.await?; if contents.is_empty() { return Ok(()); @@ -625,7 +930,18 @@ impl AcpThreadView { message_editor.clear(window, cx); }); })?; - let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + let send = thread.update(cx, |thread, cx| { + thread.action_log().update(cx, |action_log, cx| { + for buffer in tracked_buffers { + action_log.buffer_read(buffer, cx) + } + }); + drop(guard); + + telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); + + thread.send(contents, cx) + })?; send.await }); @@ -635,6 +951,16 @@ impl AcpThreadView { this.handle_thread_error(err, cx); }) .ok(); + } else { + this.update(cx, |this, cx| { + this.should_be_following = this + .workspace + .update(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or_default(); + }) + .ok(); } }) .detach(); @@ -678,20 +1004,24 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + if self.is_loading_contents { + return; + } - let Some(rewind) = thread.update(cx, |thread, cx| { - let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; - Some(thread.rewind(user_message_id, cx)) + let Some(user_message_id) = thread.update(cx, |thread, _| { + thread.entries().get(entry_ix)?.user_message()?.id.clone() }) else { return; }; - let contents = - message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); + let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); - let task = cx.foreground_executor().spawn(async move { - rewind.await?; - contents.await + let task = cx.spawn(async move |_, cx| { + let contents = contents.await?; + thread + .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? + .await?; + Ok(contents) }); self.send_impl(task, window, cx); } @@ -756,7 +1086,7 @@ impl AcpThreadView { } fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error)); + self.thread_error = Some(ThreadError::from_err(error, &self.agent)); cx.notify(); } @@ -785,7 +1115,6 @@ impl AcpThreadView { self.entry_view_state.update(cx, |view_state, cx| { view_state.sync_entry(*index, thread, window, cx) }); - self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { self.entry_view_state @@ -824,8 +1153,25 @@ impl AcpThreadView { AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } } - AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} + AcpThreadEvent::TitleUpdated => { + let title = thread.read(cx).title(); + if let Some(title_editor) = self.title_editor() { + title_editor.update(cx, |editor, cx| { + if editor.text(cx) != title { + editor.set_text(title, window, cx); + } + }); + } + } + AcpThreadEvent::PromptCapabilitiesUpdated => { + self.prompt_capabilities + .set(thread.read(cx).prompt_capabilities()); + } + AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } @@ -836,36 +1182,112 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else { + let ThreadState::Unauthenticated { + connection, + pending_auth_method, + configuration_view, + .. + } = &mut self.thread_state + else { return; }; - self.thread_error.take(); - let authenticate = connection.authenticate(method, cx); - self.auth_task = Some(cx.spawn_in(window, { - let project = self.project.clone(); - let agent = self.agent.clone(); - async move |this, cx| { - let result = authenticate.await; - - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) - } - this.auth_task.take() - }) - .ok(); + if method.0.as_ref() == "gemini-api-key" { + let registry = LanguageModelRegistry::global(cx); + let provider = registry + .read(cx) + .provider(&language_model::GOOGLE_PROVIDER_ID) + .unwrap(); + if !provider.is_authenticated(cx) { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some("GEMINI_API_KEY must be set".to_owned()), + provider_id: Some(language_model::GOOGLE_PROVIDER_ID), + }, + agent, + connection, + window, + cx, + ); + }); + return; } - })); + } else if method.0.as_ref() == "vertex-ai" + && std::env::var("GOOGLE_API_KEY").is_err() + && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() + || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err())) + { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some( + "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed." + .to_owned(), + ), + provider_id: None, + }, + agent, + connection, + window, + cx, + ) + }); + return; + } + + self.thread_error.take(); + configuration_view.take(); + pending_auth_method.replace(method.clone()); + let authenticate = connection.authenticate(method, cx); + cx.notify(); + self.auth_task = + Some(cx.spawn_in(window, { + let project = self.project.clone(); + let agent = self.agent.clone(); + async move |this, cx| { + let result = authenticate.await; + + match &result { + Ok(_) => telemetry::event!( + "Authenticate Agent Succeeded", + agent = agent.telemetry_id() + ), + Err(_) => { + telemetry::event!( + "Authenticate Agent Failed", + agent = agent.telemetry_id(), + ) + } + } + + this.update_in(cx, |this, window, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } else { + this.thread_state = Self::initial_state( + agent, + None, + this.workspace.clone(), + project.clone(), + window, + cx, + ) + } + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -873,6 +1295,7 @@ impl AcpThreadView { tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, + window: &mut Window, cx: &mut Context, ) { let Some(thread) = self.thread() else { @@ -881,6 +1304,13 @@ impl AcpThreadView { thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } cx.notify(); } @@ -924,9 +1354,24 @@ impl AcpThreadView { None }; + let has_checkpoint_button = message + .checkpoint + .as_ref() + .is_some_and(|checkpoint| checkpoint.show); + + let agent_name = self.agent.name(); + v_flex() .id(("user_message", entry_ix)) - .pt_2() + .map(|this| { + if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { + this.pt_4() + } else if rules_item.is_some() { + this.pt_3() + } else { + this.pt_2() + } + }) .pb_4() .px_2() .gap_1p5() @@ -935,6 +1380,7 @@ impl AcpThreadView { .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { h_flex() + .px_3() .gap_2() .child(Divider::horizontal()) .child( @@ -959,63 +1405,93 @@ impl AcpThreadView { div() .py_3() .px_2() - .rounded_lg() + .rounded_md() .shadow_md() .bg(cx.theme().colors().editor_background) .border_1() .when(editing && !editor_focus, |this| this.border_dashed()) .border_color(cx.theme().colors().border) .map(|this|{ - if editor_focus { + if editing && editor_focus { this.border_color(focus_border) - } else { + } else if message.id.is_some() { this.hover(|s| s.border_color(focus_border.opacity(0.8))) + } else { + this } }) .text_xs() .child(editor.clone().into_any_element()), ) - .when(editor_focus, |this| - this.child( - h_flex() - .absolute() - .top_neg_3p5() - .right_3() - .gap_1() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - .child( - IconButton::new("cancel", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(Self::cancel_editing)) - ) - .child( - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })), - ) - ) - ), + .when(editor_focus, |this| { + let base_container = h_flex() + .absolute() + .top_neg_3p5() + .right_3() + .gap_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .overflow_hidden(); + + if message.id.is_some() { + this.child( + base_container + .child( + IconButton::new("cancel", IconName::Close) + .disabled(self.is_loading_contents) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(Self::cancel_editing)) + ) + .child( + if self.is_loading_contents { + div() + .id("loading-edited-message-content") + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::XSmall)) + .into_any_element() + } else { + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })).into_any_element() + } + ) + ) + } else { + this.child( + base_container + .border_dashed() + .child( + IconButton::new("editing_unavailable", IconName::PencilUnavailable) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .style(ButtonStyle::Transparent) + .tooltip(move |_window, cx| { + cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) + .into() + }) + ) + ) + } + }), ) .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let style = default_markdown_style(false, window, cx); + let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() .gap_2p5() @@ -1055,7 +1531,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().py_1p5().px_5().map(|this| { + div().w_full().py_1().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1074,12 +1550,15 @@ impl AcpThreadView { return primary; }; - let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - let primary = if entry_ix == total_entries - 1 && !is_generating { + let primary = if entry_ix == total_entries - 1 { v_flex() .w_full() .child(primary) - .child(self.render_thread_controls(cx)) + .child(self.render_thread_controls(&thread, cx)) + .when_some( + self.thread_feedback.comments_editor.clone(), + |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), + ) .into_any_element() } else { primary @@ -1144,8 +1623,6 @@ impl AcpThreadView { .relative() .w_full() .gap_1p5() - .opacity(0.8) - .hover(|style| style.opacity(1.)) .child( h_flex() .size_4() @@ -1186,6 +1663,7 @@ impl AcpThreadView { .child( div() .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) .child("Thinking"), ) .on_click(cx.listener({ @@ -1209,75 +1687,15 @@ impl AcpThreadView { .border_l_1() .border_color(self.tool_card_border_color(cx)) .text_ui_sm(cx) - .child( - self.render_markdown(chunk, default_markdown_style(false, window, cx)), - ), + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), ) }) .into_any_element() } - fn render_tool_call_icon( - &self, - group_name: SharedString, - entry_ix: usize, - is_collapsible: bool, - is_open: bool, - tool_call: &ToolCall, - cx: &Context, - ) -> Div { - let tool_icon = Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) - .size(IconSize::Small) - .color(Color::Muted); - - let base_container = h_flex().size_4().justify_center(); - - if is_collapsible { - base_container - .child( - div() - .group_hover(&group_name, |s| s.invisible().w_0()) - .child(tool_icon), - ) - .child( - h_flex() - .absolute() - .inset_0() - .invisible() - .justify_center() - .group_hover(&group_name, |s| s.visible()) - .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronRight) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), - ), - ) - } else { - base_container.child(tool_icon) - } - } - fn render_tool_call( &self, entry_ix: usize, @@ -1288,29 +1706,37 @@ impl AcpThreadView { let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-tool-call-header"); - let status_icon = match &tool_call.status { - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Completed => None, - ToolCallStatus::InProgress => Some( - Icon::new(IconName::ArrowCircle) - .color(Color::Accent) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any(), - ), - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small) - .into_any_element(), - ), + let tool_icon = + if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { + FileIcons::get_icon(&tool_call.locations[0].path, cx) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ToolPencil)) + } else { + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolSearch, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolThink, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + } + .size(IconSize::Small) + .color(Color::Muted); + + let failed_or_canceled = match &tool_call.status { + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, + _ => false, }; + let failed_tool_call = matches!( + tool_call.status, + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed + ); + let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1319,10 +1745,9 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = - needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -1365,7 +1790,9 @@ impl AcpThreadView { .into_any() } ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit && tool_call.content.is_empty() => + if is_edit + && tool_call.content.is_empty() + && self.as_native_connection(cx).is_some() => { self.render_diff_loading(cx).into_any() } @@ -1390,7 +1817,7 @@ impl AcpThreadView { v_flex() .when(use_card_layout, |this| { - this.rounded_lg() + this.rounded_md() .border_1() .border_color(self.tool_card_border_color(cx)) .bg(cx.theme().colors().editor_background) @@ -1399,39 +1826,29 @@ impl AcpThreadView { .child( h_flex() .id(header_id) + .group(&card_header_id) + .relative() .w_full() + .max_w_full() .gap_1() - .justify_between() - .map(|this| { - if use_card_layout { - this.pl_2() - .pr_1p5() - .py_1() - .rounded_t_md() - .when(is_open, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) - .bg(self.tool_card_header_bg(cx)) - } else { - this.opacity(0.8).hover(|style| style.opacity(1.)) - } + .when(use_card_layout, |this| { + this.pl_1p5() + .pr_1() + .py_0p5() + .rounded_t_md() + .when(is_open && !failed_tool_call, |this| { + this.border_b_1() + .border_color(self.tool_card_border_color(cx)) + }) + .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() - .group(&card_header_id) .relative() .w_full() - .min_h_6() + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) - .child(self.render_tool_call_icon( - card_header_id, - entry_ix, - is_collapsible, - is_open, - tool_call, - cx, - )) + .child(tool_icon) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path @@ -1447,54 +1864,68 @@ impl AcpThreadView { .px_1p5() .rounded_sm() .overflow_x_scroll() - .opacity(0.8) .hover(|label| { - label.opacity(1.).bg(cx - .theme() - .colors() - .element_hover - .opacity(0.5)) + label.bg(cx.theme().colors().element_hover.opacity(0.5)) + }) + .map(|this| { + if use_card_layout { + this.text_color(cx.theme().colors().text) + } else { + this.text_color(cx.theme().colors().text_muted) + } }) .child(name) .tooltip(Tooltip::text("Jump to File")) + .cursor(gpui::CursorStyle::PointingHand) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { h_flex() - .id("non-card-label-container") - .w_full() .relative() + .w_full() + .max_w_full() .ml_1p5() .overflow_hidden() - .child( - h_flex() - .id("non-card-label") - .pr_8() - .w_full() - .overflow_x_scroll() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, window, cx), - )), - ) + .child(h_flex().pr_8().child(self.render_markdown( + tool_call.label.clone(), + default_markdown_style(false, true, window, cx), + ))) .child(gradient_overlay(gradient_color)) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })) .into_any() }), ) - .children(status_icon), + .child( + h_flex() + .gap_px() + .when(is_collapsible, |this| { + this.child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&card_header_id) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })), + ) + }) + .when(failed_or_canceled, |this| { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + }), + ), ) .children(tool_output_display) } @@ -1531,7 +1962,7 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone())); + let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); v_flex() .mt_1p5() @@ -1542,19 +1973,15 @@ impl AcpThreadView { .border_color(self.tool_card_border_color(cx)) .text_sm() .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) + .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) .child( - Button::new(button_id, "Collapse Output") + IconButton::new(button_id, IconName::ChevronUp) .full_width() .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ChevronUp) .icon_color(Color::Muted) - .icon_position(IconPosition::Start) .on_click(cx.listener({ - let id = tool_call_id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { - this.expanded_tool_calls.remove(&id); + this.expanded_tool_calls.remove(&tool_call_id); cx.notify(); } })), @@ -1568,14 +1995,32 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let uri: SharedString = resource_link.uri.clone().into(); + let is_file = resource_link.uri.strip_prefix("file://"); - let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { - path.to_string().into() + let label: SharedString = if let Some(abs_path) = is_file { + if let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&Path::new(abs_path), cx) + && let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + { + worktree + .read(cx) + .full_path(&project_path.path) + .to_string_lossy() + .to_string() + .into() + } else { + abs_path.to_string().into() + } } else { uri.clone() }; - let button_id = SharedString::from(format!("item-{}", uri.clone())); + let button_id = SharedString::from(format!("item-{}", uri)); div() .ml(px(7.)) @@ -1587,10 +2032,12 @@ impl AcpThreadView { Button::new(button_id, label) .label_size(LabelSize::Small) .color(Color::Muted) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) .truncate(true) + .when(is_file.is_none(), |this| { + this.icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + }) .on_click(cx.listener({ let workspace = self.workspace.clone(); move |_, _, window, cx: &mut Context| { @@ -1649,11 +2096,12 @@ impl AcpThreadView { let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, _, cx| { + move |this, _, window, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, + window, cx, ); } @@ -1721,8 +2169,8 @@ impl AcpThreadView { && let Some(editor) = entry.editor_for_diff(diff) && diff.read(cx).has_revealed_range(cx) { - editor.clone().into_any_element() - } else if tool_progress { + editor.into_any_element() + } else if tool_progress && self.as_native_connection(cx).is_some() { self.render_diff_loading(cx) } else { Empty.into_any() @@ -1775,6 +2223,8 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); + let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); + let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -1868,7 +2318,7 @@ impl AcpThreadView { .to_string() } else { format!( - "Output is {} long—to avoid unexpected token usage, \ + "Output is {} long, and to avoid unexpected token usage, \ only 16 KB was sent back to the model.", format_file_size(output.original_content_len as u64, true), ) @@ -1908,28 +2358,34 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - self.terminal_expanded, + is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.terminal_expanded = !this.terminal_expanded; - })), - ); + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + }})), + ); let terminal_view = self .entry_view_state .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = self.terminal_expanded && terminal_view.is_some(); + let show_output = is_expanded && terminal_view.is_some(); v_flex() .mb_2() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) - .rounded_lg() + .rounded_md() .overflow_hidden() .child( v_flex() @@ -1970,33 +2426,6 @@ impl AcpThreadView { .into_any() } - fn render_agent_logo(&self) -> AnyElement { - Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element() - } - - fn render_error_agent_logo(&self) -> AnyElement { - let logo = Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element(); - - h_flex() - .relative() - .justify_center() - .child(div().opacity(0.3).child(logo)) - .child( - h_flex() - .absolute() - .right_1() - .bottom_0() - .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), - ) - .into_any_element() - } - fn render_rules_item(&self, cx: &Context) -> Option { let project_context = self .as_native_thread(cx)? @@ -2044,39 +2473,30 @@ impl AcpThreadView { return None; } + let has_both = user_rules_text.is_some() && rules_file_text.is_some(); + Some( - v_flex() + h_flex() .px_2p5() - .gap_1() + .child( + Icon::new(IconName::Attach) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() - .group("user-rules") .id("user-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1() + .mr_1p5() .child( Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) - .truncate() - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("user-rules") - // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")), + .truncate(), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { window.dispatch_action( Box::new(OpenRulesLibrary { @@ -2087,33 +2507,25 @@ impl AcpThreadView { }), ) }) + .when(has_both, |this| { + this.child( + Label::new("•") + .size(LabelSize::XSmall) + .color(Color::Disabled), + ) + }) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() - .group("project-rules") .id("project-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1p5() .child( Label::new(rules_file_text) .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-rule", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("project-rules") - .tooltip(Tooltip::text("View Project Rules")), + .color(Color::Muted), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View Project Rules")) .on_click(cx.listener(Self::handle_open_rules)), ) }) @@ -2121,51 +2533,104 @@ impl AcpThreadView { ) } - fn render_empty_state(&self, cx: &App) -> AnyElement { - let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + fn render_empty_state_section_header( + &self, + label: impl Into, + action_slot: Option, + cx: &mut Context, + ) -> impl IntoElement { + div().pl_1().pr_1p5().child( + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot), + ) + } + + fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + let render_history = self + .agent + .clone() + .downcast::() + .is_some() + && self + .history_store + .update(cx, |history_store, cx| !history_store.is_empty(cx)); v_flex() .size_full() - .items_center() - .justify_center() - .child(if loading { - h_flex() - .justify_center() - .child(self.render_agent_logo()) - .with_animation( - "pulsating_icon", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 1.0)), - |icon, delta| icon.opacity(delta), - ) - .into_any() - } else { - self.render_agent_logo().into_any_element() + .when(render_history, |this| { + let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { + history_store.entries().take(3).collect() + }); + this.justify_end().child( + v_flex() + .child( + self.render_empty_state_section_header( + "Recent", + Some( + Button::new("view-history", "View All") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenHistory.boxed_clone(), cx); + }) + .into_any_element(), + ), + cx, + ), + ) + .child( + v_flex().p_1().pr_1p5().gap_1().children( + recent_history + .into_iter() + .enumerate() + .map(|(index, entry)| { + // TODO: Add keyboard navigation. + let is_hovered = + self.hovered_recent_history_item == Some(index); + crate::acp::thread_history::AcpHistoryEntryElement::new( + entry, + cx.entity().downgrade(), + ) + .hovered(is_hovered) + .on_hover(cx.listener( + move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_recent_history_item = Some(index); + } else if this.hovered_recent_history_item + == Some(index) + { + this.hovered_recent_history_item = None; + } + cx.notify(); + }, + )) + .into_any_element() + }), + ), + ), + ) }) - .child(h_flex().mt_4().mb_1().justify_center().child(if loading { - div() - .child(LoadingLabel::new("").size(LabelSize::Large)) - .into_any_element() - } else { - Headline::new(self.agent.empty_state_headline()) - .size(HeadlineSize::Medium) - .into_any_element() - })) - .child( - div() - .max_w_1_2() - .text_sm() - .text_center() - .map(|this| { - if loading { - this.invisible() - } else { - this.text_color(cx.theme().colors().text_muted) - } - }) - .child(self.agent.empty_state_message()), - ) .into_any() } @@ -2174,132 +2639,149 @@ impl AcpThreadView { connection: &Rc, description: Option<&Entity>, configuration_view: Option<&AnyView>, + pending_auth_method: Option<&acp::AuthMethodId>, window: &mut Window, cx: &Context, ) -> Div { - v_flex() - .p_2() - .gap_2() - .flex_1() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child(h_flex().mt_4().mb_1().justify_center().child( - Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), - )) - .into_any(), - ) - .children(description.map(|desc| { - div().text_ui(cx).text_center().child( - self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)), + let show_description = + configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); + + v_flex().flex_1().size_full().justify_end().child( + v_flex() + .p_2() + .pr_3() + .w_full() + .gap_1() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().status().warning.opacity(0.04)) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::Small), + ) + .child(Label::new("Authentication Required").size(LabelSize::Small)), ) - })) - .children( - configuration_view - .cloned() - .map(|view| div().px_4().w_full().max_w_128().child(view)), - ) - .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().iter().map(|method| { - Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) + .children(description.map(|desc| { + div().text_ui(cx).child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) + })) + .children( + configuration_view + .cloned() + .map(|view| div().w_full().child(view)), + ) + .when( + show_description, + |el| { + el.child( + Label::new(format!( + "You are not currently authenticated with {}. Please choose one of the following options:", + self.agent.name() + )) + .size(LabelSize::Small) + .color(Color::Muted) + .mb_1() + .ml_5(), + ) + }, + ) + .when_some(pending_auth_method, |el, _| { + el.child( + h_flex() + .py_4() + .w_full() + .justify_center() + .gap_1() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ) + .into_any_element(), + ) + .child(Label::new("Authenticating…").size(LabelSize::Small)), + ) + }) + .when(!connection.auth_methods().is_empty(), |this| { + this.child( + h_flex() + .justify_end() + .flex_wrap() + .gap_1() + .when(!show_description, |this| { + this.border_t_1() + .mt_1() + .pt_2() + .border_color(cx.theme().colors().border.opacity(0.8)) }) - }) - }), - )) + .children( + connection + .auth_methods() + .iter() + .enumerate() + .rev() + .map(|(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + telemetry::event!( + "Authenticate Agent Started", + agent = this.agent.telemetry_id(), + method = method_id + ); + + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + ), + ) + }) + + ) } fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { - let mut container = v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - v_flex() - .mt_4() - .mb_2() - .gap_0p5() - .text_center() - .items_center() - .child(Headline::new("Failed to launch").size(HeadlineSize::Medium)) - .child( - Label::new(e.to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ); - - if let LoadError::Unsupported { - upgrade_message, - upgrade_command, - .. - } = &e - { - let upgrade_message = upgrade_message.clone(); - let upgrade_command = upgrade_command.clone(); - container = container.child( - Button::new("upgrade", upgrade_message) - .tooltip(Tooltip::text(upgrade_command.clone())) - .on_click(cx.listener(move |this, _, window, cx| { - let task = this - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("upgrade".to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), - args: Vec::new(), - command_label: upgrade_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - })), - ); - } else if let LoadError::NotInstalled { - install_message, - install_command, - .. - } = e - { - let install_message = install_message.clone(); - let install_command = install_command.clone(); - container = container.child( - Button::new("install", install_message) + let (message, action_slot) = match e { + LoadError::NotInstalled { + error_message, + install_message, + install_command, + } => { + let install_command = install_command.clone(); + let button = Button::new("install", install_message) .tooltip(Tooltip::text(install_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); + let task = this .workspace .update(cx, |workspace, cx| { @@ -2307,7 +2789,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), + id: task::TaskId(install_command.clone()), full_label: install_command.clone(), label: install_command.clone(), command: Some(install_command.clone()), @@ -2338,11 +2820,83 @@ impl AcpThreadView { } }) .detach() - })), - ); - } + })); - container.into_any() + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Unsupported { + error_message, + upgrade_message, + upgrade_command, + } => { + let upgrade_command = upgrade_command.clone(); + let button = Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); + + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(upgrade_command.to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })); + + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), + LoadError::Other(msg) => ( + msg.into(), + Some(self.create_copy_button(msg.to_string()).into_any_element()), + ), + }; + + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircleFilled) + .title("Failed to Launch") + .description(message) + .actions_slot(div().children(action_slot)) + .into_any_element() } fn render_activity_bar( @@ -2391,7 +2945,6 @@ impl AcpThreadView { }) .when(!changed_buffers.is_empty(), |this| { this.child(self.render_edits_summary( - action_log, &changed_buffers, self.edits_expanded, pending_edits, @@ -2543,7 +3096,6 @@ impl AcpThreadView { fn render_edits_summary( &self, - action_log: &Entity, changed_buffers: &BTreeMap, Entity>, expanded: bool, pending_edits: bool, @@ -2557,13 +3109,13 @@ impl AcpThreadView { h_flex() .p_1() .justify_between() + .flex_wrap() .when(expanded, |this| { this.border_b_1().border_color(cx.theme().colors().border) }) .child( h_flex() .id("edits-container") - .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) .map(|this| { @@ -2654,14 +3206,9 @@ impl AcpThreadView { ) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.reject_all_edits(cx).detach(); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.reject_all(&RejectAll, window, cx); + })), ) .child( Button::new("keep-all-changes", "Keep All") @@ -2674,14 +3221,9 @@ impl AcpThreadView { KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.keep_all_edits(cx); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.keep_all(&KeepAll, window, cx); + })), ), ) } @@ -2845,6 +3387,19 @@ impl AcpThreadView { (IconName::Maximize, "Expand Message Editor") }; + let backdrop = div() + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll(); + + let enable_editor = match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, + ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, + }; + v_flex() .on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { @@ -2885,7 +3440,6 @@ impl AcpThreadView { .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( expand_tooltip, @@ -2921,6 +3475,7 @@ impl AcpThreadView { .child(self.render_send_button(cx)), ), ) + .when(!enable_editor, |this| this.child(backdrop)) .into_any() } @@ -2938,6 +3493,12 @@ impl AcpThreadView { .thread(acp_thread.session_id(), cx) } + fn is_using_zed_ai_models(&self, cx: &App) -> bool { + self.as_native_thread(cx) + .and_then(|thread| thread.read(cx).model()) + .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID) + } + fn render_token_usage(&self, cx: &mut Context) -> Option
{ let thread = self.thread()?.read(cx); let usage = thread.token_usage()?; @@ -2962,7 +3523,7 @@ impl AcpThreadView { "used-tokens-label", Animation::new(Duration::from_secs(2)) .repeat() - .with_easing(pulsating_between(0.6, 1.)), + .with_easing(pulsating_between(0.3, 0.8)), |label, delta| label.alpha(delta), ) .into_any() @@ -3002,6 +3563,24 @@ impl AcpThreadView { }); } + fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx)); + } + + fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log + .update(cx, |action_log, cx| action_log.reject_all_edits(cx)) + .detach(); + } + fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { let thread = self.as_native_thread(cx)?.read(cx); @@ -3043,7 +3622,14 @@ impl AcpThreadView { .thread() .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - if is_generating && is_editor_empty { + if self.is_loading_contents { + div() + .id("loading-message-content") + .px_1() + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::default())) + .into_any_element() + } else if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -3078,13 +3664,53 @@ impl AcpThreadView { } } + fn is_following(&self, cx: &App) -> bool { + match self.thread().map(|thread| thread.read(cx).status()) { + Some(ThreadStatus::Generating) => self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false), + _ => self.should_be_following, + } + } + + fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { + let following = self.is_following(cx); + + self.should_be_following = !following; + if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) { + self.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); + } + + telemetry::event!("Follow Agent Selected", following = !following); + } + fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { - let following = self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or(false); + let following = self.is_following(cx); + + let tooltip_label = if following { + if self.agent.name() == "Zed Agent" { + format!("Stop Following the {}", self.agent.name()) + } else { + format!("Stop Following {}", self.agent.name()) + } + } else { + if self.agent.name() == "Zed Agent" { + format!("Follow the {}", self.agent.name()) + } else { + format!("Follow {}", self.agent.name()) + } + }; IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3093,10 +3719,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action("Stop Following Agent", &Follow, window, cx) + Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) } else { Tooltip::with_meta( - "Follow Agent", + tooltip_label.clone(), Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -3105,15 +3731,7 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); + this.toggle_following(window, cx); })) } @@ -3149,6 +3767,7 @@ impl AcpThreadView { .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } + MentionUri::PastedImage => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); let Some(entry) = project.update(cx, |project, cx| { @@ -3163,9 +3782,14 @@ impl AcpThreadView { }); } MentionUri::Symbol { - path, line_range, .. + abs_path: path, + line_range, + .. } - | MentionUri::Selection { path, line_range } => { + | MentionUri::Selection { + abs_path: Some(path), + line_range, + } => { let project = workspace.project(); let Some((path, _)) = project.update(cx, |project, cx| { let path = project.find_project_path(path, cx)?; @@ -3181,8 +3805,8 @@ impl AcpThreadView { let Some(editor) = item.await?.downcast::() else { return Ok(()); }; - let range = - Point::new(line_range.start, 0)..Point::new(line_range.start, 0); + let range = Point::new(*line_range.start(), 0) + ..Point::new(*line_range.start(), 0); editor .update_in(cx, |editor, window, cx| { editor.change_selections( @@ -3197,6 +3821,7 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } + MentionUri::Selection { abs_path: None, .. } => {} MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { @@ -3399,7 +4024,8 @@ impl AcpThreadView { return; } - let title = self.title(cx); + // TODO: Change this once we have title summarization for external agents. + let title = self.agent.name(); match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { @@ -3517,7 +4143,20 @@ impl AcpThreadView { } } - fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { + fn render_thread_controls( + &self, + thread: &Entity, + cx: &Context, + ) -> impl IntoElement { + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + if is_generating { + return h_flex().id("thread-controls-container").ml_1().child( + div() + .py_2() + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)), + ); + } let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -3539,17 +4178,236 @@ impl AcpThreadView { this.scroll_to_top(cx); })); - h_flex() + let mut container = h_flex() + .id("thread-controls-container") + .group("thread-controls-container") .w_full() .mr_1() + .pt_1() .pb_2() .px(RESPONSE_PADDING_X) + .gap_px() .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) + .justify_end(); + + if AgentSettings::get_global(cx).enable_feedback + && self + .thread() + .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) + { + let feedback = self.thread_feedback.feedback; + container = container.child( + div().visible_on_hover("thread-controls-container").child( + Label::new( + match feedback { + Some(ThreadFeedback::Positive) => "Thanks for your feedback!", + Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", + None => "Rating the thread sends all of your current conversation to the Zed team.", + } + ) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ).child( + h_flex() + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Positive, + window, + cx, + ); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Negative, + window, + cx, + ); + })), + ) + ) + } + + container.child(open_as_markdown).child(scroll_to_top) + } + + fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { + h_flex() + .key_context("AgentFeedbackMessageEditor") + .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })) + .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { + this.submit_feedback_message(cx); + })) + .p_2() + .mb_2() + .mx_5() + .gap_1() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .child(div().w_full().child(editor)) + .child( + h_flex() + .child( + IconButton::new("dismiss-feedback-message", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(cx.listener(move |this, _, _window, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })), + ) + .child( + IconButton::new("submit-feedback-message", IconName::Return) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(cx.listener(move |this, _, _window, cx| { + this.submit_feedback_message(cx); + })), + ), + ) + } + + fn handle_feedback_click( + &mut self, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit(thread, feedback, window, cx); + cx.notify(); + } + + fn submit_feedback_message(&mut self, cx: &mut Context) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit_comments(thread, cx); + cx.notify(); + } + + fn render_token_limit_callout( + &self, + line_height: Pixels, + cx: &mut Context, + ) -> Option { + let token_usage = self.thread()?.read(cx).token_usage()?; + let ratio = token_usage.ratio(); + + let (severity, title) = match ratio { + acp_thread::TokenUsageRatio::Normal => return None, + acp_thread::TokenUsageRatio::Warning => { + (Severity::Warning, "Thread reaching the token limit soon") + } + acp_thread::TokenUsageRatio::Exceeded => { + (Severity::Error, "Thread reached the token limit") + } + }; + + let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| { + thread.read(cx).completion_mode() == CompletionMode::Normal + && thread + .read(cx) + .model() + .is_some_and(|model| model.supports_burn_mode()) + }); + + let description = if burn_mode_available { + "To continue, start a new thread from a summary or turn Burn Mode on." + } else { + "To continue, start a new thread from a summary." + }; + + Some( + Callout::new() + .severity(severity) + .line_height(line_height) + .title(title) + .description(description) + .actions_slot( + h_flex() + .gap_0p5() + .child( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + let Some(thread) = this.thread() else { + return; + }; + let session_id = thread.read(cx).session_id().clone(); + window.dispatch_action( + crate::NewNativeAgentThreadFromSummary { + from_session_id: session_id, + } + .boxed_clone(), + cx, + ); + })), + ) + .when(burn_mode_available, |this| { + this.child( + IconButton::new("burn-mode-callout", IconName::ZedBurnMode) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })), + ) + }), + ), + ) + } + + fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option
{ + if !self.is_using_zed_ai_models(cx) { + return None; + } + + let user_store = self.project.read(cx).user_store().read(cx); + if user_store.is_usage_based_billing_enabled() { + return None; + } + + let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); + + let usage = user_store.model_request_usage()?; + + Some( + div() + .child(UsageCallout::new(plan, usage)) + .line_height(line_height), + ) } fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { @@ -3570,6 +4428,12 @@ impl AcpThreadView { }) } + pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_selections(window, cx); + }) + } + fn render_thread_retry_status_callout( &self, _window: &mut Window, @@ -3615,6 +4479,9 @@ impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::AuthenticationRequired(error) => { + self.render_authentication_required_error(error.clone(), cx) + } ThreadError::PaymentRequired => self.render_payment_required_error(cx), ThreadError::ModelRequestLimitReached(plan) => { self.render_model_request_limit_reached_error(*plan, cx) @@ -3628,11 +4495,53 @@ impl AcpThreadView { } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + let can_resume = self + .thread() + .map_or(false, |thread| thread.read(cx).can_resume(cx)); + + let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| { + let thread = thread.read(cx); + let supports_burn_mode = thread + .model() + .map_or(false, |model| model.supports_burn_mode()); + supports_burn_mode && thread.completion_mode() == CompletionMode::Normal + }); + Callout::new() .severity(Severity::Error) .title("Error") + .icon(IconName::XCircle) .description(error.clone()) - .actions_slot(self.create_copy_button(error.to_string())) + .actions_slot( + h_flex() + .gap_0p5() + .when(can_resume && can_enable_burn_mode, |this| { + this.child( + Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + this.resume_chat(cx); + })), + ) + }) + .when(can_resume, |this| { + this.child( + Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.resume_chat(cx); + })), + ) + }) + .child(self.create_copy_button(error.to_string())), + ) .dismiss_action(self.dismiss_error_button(cx)) } @@ -3642,6 +4551,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) + .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( @@ -3653,6 +4563,25 @@ impl AcpThreadView { .dismiss_action(self.dismiss_error_button(cx)) } + fn render_authentication_required_error( + &self, + error: SharedString, + cx: &mut Context, + ) -> Callout { + Callout::new() + .severity(Severity::Error) + .title("Authentication Required") + .icon(IconName::XCircle) + .description(error.clone()) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.authenticate_button(cx)) + .child(self.create_copy_button(error)), + ) + .dismiss_action(self.dismiss_error_button(cx)) + } + fn render_model_request_limit_reached_error( &self, plan: cloud_llm_client::Plan, @@ -3668,6 +4597,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") + .icon(IconName::XCircle) .description(error_message) .actions_slot( h_flex() @@ -3774,6 +4704,49 @@ impl AcpThreadView { })) } + fn authenticate_button(&self, cx: &mut Context) -> impl IntoElement { + Button::new("authenticate", "Authenticate") + .label_size(LabelSize::Small) + .style(ButtonStyle::Filled) + .on_click(cx.listener({ + move |this, _, window, cx| { + let agent = this.agent.clone(); + let ThreadState::Ready { thread, .. } = &this.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + this.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + })) + } + + pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { + let agent = self.agent.clone(); + let ThreadState::Ready { thread, .. } = &self.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + self.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) @@ -3797,90 +4770,113 @@ impl AcpThreadView { ); cx.notify(); } + + pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context) { + let task = match entry { + HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { + history.delete_thread(thread.id.clone(), cx) + }), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| { + history.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } +} + +fn loading_contents_spinner(size: IconSize) -> AnyElement { + Icon::new(IconName::LoadCircle) + .size(size) + .color(Color::Accent) + .with_animation( + "load_context_circle", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element() } impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) + match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => { + self.message_editor.focus_handle(cx) + } + ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { + self.focus_handle.clone() + } + } } } impl Render for AcpThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_messages = self.list_state.item_count() > 0; + let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; v_flex() .size_full() .key_context("AcpThread") .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(Self::keep_all)) + .on_action(cx.listener(Self::reject_all)) + .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { connection, description, configuration_view, + pending_auth_method, .. } => self .render_auth_required_state( connection, description.as_ref(), configuration_view.as_ref(), + pending_auth_method.as_ref(), window, cx, ) .into_any(), ThreadState::Loading { .. } => v_flex() .flex_1() - .child(self.render_empty_state(cx)) + .child(self.render_recent_history(window, cx)) .into_any(), ThreadState::LoadError(e) => v_flex() - .p_2() .flex_1() + .size_full() .items_center() - .justify_center() + .justify_end() .child(self.render_load_error(e, cx)) .into_any(), - ThreadState::Ready { thread, .. } => { - let thread_clone = thread.clone(); - - v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), + ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { + if has_messages { + this.child( + list( + self.list_state.clone(), + cx.processor(|this, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + }), ) - .vertical_scrollbar(window, cx) - .children(match thread_clone.read(cx).status() { - ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { - None - } - ThreadStatus::Generating => div() - .px_5() - .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) - .into(), - }) - .children(self.render_activity_bar(&thread_clone, window, cx)) - .into_any_element() - } else { - this.child(self.render_empty_state(cx)).into_any_element() - } - }) - } + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) + .into_any() + } else { + this.child(self.render_recent_history(window, cx)) + .into_any() + } + }), }) // The activity bar is intentionally rendered outside of the ThreadState::Ready match // above so that the scrollbar doesn't render behind it. The current setup allows @@ -3893,11 +4889,24 @@ impl Render for AcpThreadView { }) .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) + .children( + if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { + Some(usage_callout.into_any_element()) + } else { + self.render_token_limit_callout(line_height, cx) + .map(|token_limit_callout| token_limit_callout.into_any_element()) + }, + ) .child(self.render_message_editor(window, cx)) } } -fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { +fn default_markdown_style( + buffer_font: bool, + muted_text: bool, + window: &Window, + cx: &App, +) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -3918,20 +4927,26 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd TextSize::Default.rems(cx) }; + let text_color = if muted_text { + colors.text_muted + } else { + colors.text + }; + text_style.refine(&TextStyleRefinement { font_family: Some(font_family), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(font_size.into()), line_height: Some(line_height.into()), - color: Some(cx.theme().colors().text), + color: Some(text_color), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().colors().element_selection_background, + selection_background_color: colors.element_selection_background, code_block_overflow_x_scroll: true, table_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { @@ -4017,7 +5032,7 @@ fn plan_label_markdown_style( window: &Window, cx: &App, ) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, window, cx); + let default_md_style = default_markdown_style(false, false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -4037,7 +5052,7 @@ fn plan_label_markdown_style( } fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, window, cx); + let default_md_style = default_markdown_style(true, false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -4196,6 +5211,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(agent), None, + None, workspace.downgrade(), project, history_store, @@ -4243,7 +5259,7 @@ pub(crate) mod tests { impl Focusable for ThreadViewItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -4277,20 +5293,24 @@ pub(crate) mod tests { where C: 'static + AgentConnection + Send + Clone, { + fn telemetry_id(&self) -> &'static str { + "test" + } + fn logo(&self) -> ui::IconName { ui::IconName::Ai } - fn name(&self) -> &'static str { - "Test" + fn name(&self) -> SharedString { + "Test".into() } - fn empty_state_headline(&self) -> &'static str { - "Test" + fn empty_state_headline(&self) -> SharedString { + "Test".into() } - fn empty_state_message(&self) -> &'static str { - "Test" + fn empty_state_message(&self) -> SharedString { + "Test".into() } fn connect( @@ -4325,6 +5345,12 @@ pub(crate) mod tests { project, action_log, SessionId("test".into()), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }))) } @@ -4402,6 +5428,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), None, + None, workspace.downgrade(), project.clone(), history_store.clone(), diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index cfa0db01d5..575d8a3a56 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -488,7 +488,7 @@ fn render_markdown_code_block( .on_click({ let active_thread = active_thread.clone(); let parsed_markdown = parsed_markdown.clone(); - let code_block_range = metadata.content_range.clone(); + let code_block_range = metadata.content_range; move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.copied_code_block_ids.insert((message_id, ix)); @@ -529,7 +529,6 @@ fn render_markdown_code_block( "Expand Code" })) .on_click({ - let active_thread = active_thread.clone(); move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.toggle_codeblock_expanded(message_id, ix); @@ -777,13 +776,11 @@ impl ActiveThread { let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); - let workspace_subscription = if let Some(workspace) = workspace.upgrade() { - Some(cx.observe_release(&workspace, |this, _, cx| { + let workspace_subscription = workspace.upgrade().map(|workspace| { + cx.observe_release(&workspace, |this, _, cx| { this.dismiss_notifications(cx); - })) - } else { - None - }; + }) + }); let mut this = Self { language_registry, @@ -912,7 +909,7 @@ impl ActiveThread { ) { let rendered = self .rendered_tool_uses - .entry(tool_use_id.clone()) + .entry(tool_use_id) .or_insert_with(|| RenderedToolUse { label: cx.new(|cx| { Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) @@ -1214,7 +1211,7 @@ impl ActiveThread { match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title.clone(), window, primary, cx); + self.pop_up(icon, caption.into(), title, window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { @@ -1594,11 +1591,6 @@ impl ActiveThread { return; }; - if model.provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let edited_text = state.editor.read(cx).text(cx); let creases = state.editor.update(cx, extract_message_creases); @@ -1761,7 +1753,7 @@ impl ActiveThread { .thread .read(cx) .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); telemetry::event!( @@ -2108,7 +2100,7 @@ impl ActiveThread { .gap_1() .children(message_content) .when_some(editing_message_state, |this, state| { - let focus_handle = state.editor.focus_handle(cx).clone(); + let focus_handle = state.editor.focus_handle(cx); this.child( h_flex() @@ -2169,7 +2161,6 @@ impl ActiveThread { .icon_color(Color::Muted) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Regenerate", @@ -2308,7 +2299,7 @@ impl ActiveThread { .into_any_element() } else if let Some(error) = error { restore_checkpoint_button - .tooltip(Tooltip::text(error.to_string())) + .tooltip(Tooltip::text(error)) .into_any_element() } else { restore_checkpoint_button.into_any_element() @@ -2349,7 +2340,6 @@ impl ActiveThread { this.submit_feedback_message(message_id, cx); cx.notify(); })) - .on_action(cx.listener(Self::confirm_editing_message)) .mb_2() .mx_4() .p_2() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 946a495f33..2b2cec5539 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,6 +5,7 @@ mod tool_picker; use std::{sync::Arc, time::Duration}; +use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; @@ -15,7 +16,7 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, - Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, + Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -23,10 +24,11 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ + Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use settings::{Settings, update_settings_file}; +use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*, @@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, + AddContextServer, ExternalAgent, NewExternalAgentThread, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -47,6 +49,7 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -55,6 +58,8 @@ pub struct AgentConfiguration { tools: Entity, _registry_subscription: Subscription, scroll_handle: ScrollHandle, + gemini_is_installed: bool, + _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -64,6 +69,7 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -88,31 +94,32 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); + cx.observe_global_in::(window, |this, _, cx| { + this.check_for_gemini(cx); + cx.notify(); + }) + .detach(); let scroll_handle = ScrollHandle::new(); - let mut expanded_provider_configurations = HashMap::default(); - if LanguageModelRegistry::read_global(cx) - .provider(&ZED_CLOUD_PROVIDER_ID) - .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) - { - expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); - } - let mut this = Self { fs, language_registry, workspace, + project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), - expanded_provider_configurations, + expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, scroll_handle, + gemini_is_installed: false, + _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); + this.check_for_gemini(cx); this } @@ -142,6 +149,34 @@ impl AgentConfiguration { self.configuration_views_by_provider .insert(provider.id(), configuration_view); } + + fn check_for_gemini(&mut self, cx: &mut Context) { + let project = self.project.clone(); + let settings = AllAgentServersSettings::get_global(cx).clone(); + self._check_for_gemini = cx.spawn({ + async move |this, cx| { + let Some(project) = project.upgrade() else { + return; + }; + let gemini_is_installed = AgentServerCommand::resolve( + Gemini::binary_name(), + &[], + // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here + None, + settings.gemini, + &project, + cx, + ) + .await + .is_some(); + this.update(cx, |this, cx| { + this.gemini_is_installed = gemini_is_installed; + cx.notify(); + }) + .ok(); + } + }); + } } impl Focusable for AgentConfiguration { @@ -162,8 +197,8 @@ impl AgentConfiguration { provider: &Arc, cx: &mut Context, ) -> impl IntoElement + use<> { - let provider_id = provider.id().0.clone(); - let provider_name = provider.name().0.clone(); + let provider_id = provider.id().0; + let provider_name = provider.name().0; let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); let configuration_view = self @@ -189,7 +224,7 @@ impl AgentConfiguration { let is_signed_in = self .workspace .read_with(cx, |workspace, _| { - workspace.client().status().borrow().is_connected() + !workspace.client().status().borrow().is_signed_out() }) .unwrap_or(false); @@ -216,7 +251,6 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) - .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -236,10 +270,7 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child( - Label::new(provider_name.clone()) - .size(LabelSize::Large), - ) + .child(Label::new(provider_name.clone())) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -266,7 +297,7 @@ impl AgentConfiguration { .closed_icon(IconName::ChevronDown), ) .on_click(cx.listener({ - let provider_id = provider.id().clone(); + let provider_id = provider.id(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations @@ -284,7 +315,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Plus) + .icon(IconName::Thread) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -383,7 +414,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features.") + Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") .color(Color::Muted), ), ), @@ -524,6 +555,14 @@ impl AgentConfiguration { } } + fn card_item_bg_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().background.opacity(0.25) + } + + fn card_item_border_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().border.opacity(0.6) + } + fn render_context_servers_section( &mut self, window: &mut Window, @@ -541,7 +580,12 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), + .child( + Label::new( + "All context servers connected through the Model Context Protocol.", + ) + .color(Color::Muted), + ), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -551,7 +595,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_2() + .gap_1p5() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -642,8 +686,6 @@ impl AgentConfiguration { .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); - let border_color = cx.theme().colors().border.opacity(0.6); - let (source_icon, source_tooltip) = if is_from_extension { ( IconName::ZedMcpExtension, @@ -662,7 +704,7 @@ impl AgentConfiguration { .size(IconSize::XSmall) .color(Color::Accent) .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), + SharedString::from(format!("{}-starting", context_server_id.0,)), Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) @@ -786,8 +828,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(border_color) - .bg(cx.theme().colors().background.opacity(0.2)) + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) .overflow_hidden() .child( h_flex() @@ -795,7 +837,11 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| element.border_b_1().border_color(border_color), + |element| { + element + .border_b_1() + .border_color(self.card_item_border_color(cx)) + }, ) .child( h_flex() @@ -862,7 +908,6 @@ impl AgentConfiguration { .on_click({ let context_server_manager = self.context_server_store.clone(); - let context_server_id = context_server_id.clone(); let fs = self.fs.clone(); move |state, _window, cx| { @@ -978,6 +1023,166 @@ impl AgentConfiguration { )) }) } + + fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { + let settings = AllAgentServersSettings::get_global(cx).clone(); + let user_defined_agents = settings + .custom + .iter() + .map(|(name, settings)| { + self.render_agent_server( + IconName::Ai, + name.clone(), + ExternalAgent::Custom { + name: name.clone(), + settings: settings.clone(), + }, + None, + cx, + ) + .into_any_element() + }) + .collect::>(); + + v_flex() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .pr(DynamicSpacing::Base20.rems(cx)) + .gap_2() + .child( + v_flex() + .gap_0p5() + .child(Headline::new("External Agents")) + .child( + Label::new( + "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", + ) + .color(Color::Muted), + ), + ) + .child(self.render_agent_server( + IconName::AiGemini, + "Gemini CLI", + ExternalAgent::Gemini, + (!self.gemini_is_installed).then_some(Gemini::install_command().into()), + cx, + )) + // TODO add CC + .children(user_defined_agents), + ) + } + + fn render_agent_server( + &self, + icon: IconName, + name: impl Into, + agent: ExternalAgent, + install_command: Option, + cx: &mut Context, + ) -> impl IntoElement { + let name = name.into(); + h_flex() + .p_1() + .pl_2() + .gap_1p5() + .justify_between() + .border_1() + .rounded_md() + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) + .overflow_hidden() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(name.clone())), + ) + .map(|this| { + if let Some(install_command) = install_command { + this.child( + Button::new( + SharedString::from(format!("install_external_agent-{name}")), + "Install Agent", + ) + .label_size(LabelSize::Small) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener( + move |this, _, window, cx| { + let Some(project) = this.project.upgrade() else { + return; + }; + let Some(workspace) = this.workspace.upgrade() else { + return; + }; + let cwd = project.read(cx).first_project_directory(cx); + let shell = + project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(install_command.to_string()), + full_label: install_command.to_string(), + label: install_command.to_string(), + command: Some(install_command.to_string()), + args: Vec::new(), + command_label: install_command.to_string(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + let task = workspace.update(cx, |workspace, cx| { + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }); + cx.spawn(async move |this, cx| { + task.await; + this.update(cx, |this, cx| { + this.check_for_gemini(cx); + }) + .ok(); + }) + .detach(); + }, + )), + ) + } else { + this.child( + h_flex().gap_1().child( + Button::new( + SharedString::from(format!("start_acp_thread-{name}")), + "Start New Thread", + ) + .label_size(LabelSize::Small) + .icon(IconName::Thread) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(agent.clone()), + } + .boxed_clone(), + cx, + ); + }), + ), + ) + } + }) + } } impl Render for AgentConfiguration { @@ -997,6 +1202,7 @@ impl Render for AgentConfiguration { .size_full() .overflow_y_scroll() .child(self.render_general_settings_section(cx)) + .child(self.render_agent_servers_section(cx)) .child(self.render_context_servers_section(window, cx)) .child(self.render_provider_configuration_section(cx)), ) @@ -1047,7 +1253,6 @@ fn show_unable_to_uninstall_extension_with_context_server( cx, move |this, _cx| { let workspace_handle = workspace_handle.clone(); - let context_server_id = context_server_id.clone(); this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) .dismiss_button(true) diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 998641bf01..182831f488 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -668,10 +668,10 @@ mod tests { ); let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -693,10 +693,10 @@ mod tests { model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, false); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(!parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -719,10 +719,10 @@ mod tests { let parsed_model = model_input.parse(cx).unwrap(); assert_eq!(parsed_model.name, "somemodel"); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 6159b9be80..c898a5acb5 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -261,7 +261,6 @@ impl ConfigureContextServerModal { _cx: &mut Context, ) { workspace.register_action({ - let language_registry = language_registry.clone(); move |_workspace, _: &AddContextServer, window, cx| { let workspace_handle = cx.weak_entity(); let language_registry = language_registry.clone(); diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 09ad013d1c..7fcf76d1cb 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -464,7 +464,7 @@ impl ManageProfilesModal { }, )) .child(ListSeparator) - .child(h_flex().p_2().child(mode.name_editor.clone())) + .child(h_flex().p_2().child(mode.name_editor)) } fn render_view_profile( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b20b126d9b..1e1ff95178 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -185,7 +185,7 @@ impl AgentDiffPane { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let project = thread.project(cx).clone(); + let project = thread.project(cx); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); @@ -196,7 +196,7 @@ impl AgentDiffPane { editor }); - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let mut this = Self { _subscriptions: vec![ @@ -1312,7 +1312,7 @@ impl AgentDiff { let entity = cx.new(|_cx| Self::default()); let global = AgentDiffGlobal(entity.clone()); cx.set_global(global); - entity.clone() + entity }) } @@ -1334,7 +1334,7 @@ impl AgentDiff { window: &mut Window, cx: &mut Context, ) { - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let action_log_subscription = cx.observe_in(&action_log, window, { let workspace = workspace.clone(); @@ -1529,6 +1529,7 @@ impl AgentDiff { | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Retry(_) => {} } } @@ -1544,7 +1545,7 @@ impl AgentDiff { && let Some(editor) = item.downcast::() && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { - self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); + self.register_editor(workspace.downgrade(), buffer, editor, window, cx); } } @@ -1643,7 +1644,7 @@ impl AgentDiff { continue; }; - for (weak_editor, _) in buffer_editors { + for weak_editor in buffer_editors.keys() { let Some(editor) = weak_editor.upgrade() else { continue; }; diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index b989e7bf1e..3de1027d91 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -66,10 +66,8 @@ impl AgentModelSelector { fs.clone(), cx, move |settings, _cx| { - settings.set_inline_assistant_model( - provider.clone(), - model_id.clone(), - ); + settings + .set_inline_assistant_model(provider.clone(), model_id); }, ); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b857052d69..269aec3365 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,9 +5,12 @@ use std::sync::Arc; use std::time::Duration; use acp_thread::AcpThread; +use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; +use zed_actions::OpenBrowser; +use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; @@ -30,7 +33,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; -use crate::{ExternalAgent, NewExternalAgentThread}; +use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -54,9 +57,7 @@ use gpui::{ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; -use language_model::{ - ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, -}; +use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; @@ -98,6 +99,16 @@ pub fn init(cx: &mut App) { workspace.focus_panel::(window, cx); } }) + .register_action( + |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.new_native_agent_thread_from_summary(action, window, cx) + }); + workspace.focus_panel::(window, cx); + } + }, + ) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -120,7 +131,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, window, cx) + panel.external_thread(action.agent.clone(), None, None, window, cx) }); } }) @@ -231,7 +242,8 @@ enum WhichFontSize { None, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +// TODO unify this with ExternalAgent +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, @@ -239,23 +251,29 @@ pub enum AgentType { Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl AgentType { - fn label(self) -> impl Into { + fn label(&self) -> SharedString { match self { - Self::Zed | Self::TextThread => "Zed Agent", - Self::NativeAgent => "Agent 2", - Self::Gemini => "Gemini CLI", - Self::ClaudeCode => "Claude Code", + Self::Zed | Self::TextThread => "Zed Agent".into(), + Self::NativeAgent => "Agent 2".into(), + Self::Gemini => "Gemini CLI".into(), + Self::ClaudeCode => "Claude Code".into(), + Self::Custom { name, .. } => name.into(), } } - fn icon(self) -> Option { + fn icon(&self) -> Option { match self { Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), + Self::Custom { .. } => Some(IconName::Terminal), } } } @@ -509,7 +527,7 @@ pub struct AgentPanel { impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent; + let selected_agent = self.selected_agent.clone(); self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( @@ -523,6 +541,7 @@ impl AgentPanel { anyhow::Ok(()) })); } + pub fn load( workspace: WeakEntity, prompt_builder: Arc, @@ -572,6 +591,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( @@ -587,7 +617,7 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { - panel.selected_agent = selected_agent; + panel.selected_agent = selected_agent.clone(); panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); @@ -658,6 +688,7 @@ impl AgentPanel { this.external_thread( Some(crate::ExternalAgent::NativeAgent), Some(thread.clone()), + None, window, cx, ); @@ -880,6 +911,16 @@ impl AgentPanel { } } + fn active_thread_view(&self) -> Option<&Entity> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view), + ActiveView::Thread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + } + } + fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context) { if cx.has_flag::() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); @@ -956,13 +997,38 @@ impl AgentPanel { message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } + fn new_native_agent_thread_from_summary( + &mut self, + action: &NewNativeAgentThreadFromSummary, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self + .acp_history_store + .read(cx) + .thread_from_session_id(&action.from_session_id) + else { + return; + }; + + self.external_thread( + Some(ExternalAgent::NativeAgent), + None, + Some(thread.clone()), + window, + cx, + ); + } + fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { + telemetry::event!("Agent Thread Started", agent = "zed-text"); + let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -1003,6 +1069,7 @@ impl AgentPanel { &mut self, agent_choice: Option, resume_thread: Option, + summarize_thread: Option, window: &mut Window, cx: &mut Context, ) { @@ -1022,14 +1089,17 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { - cx.background_spawn(async move { - if let Some(serialized) = - serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() - { - KEY_VALUE_STORE - .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) - .await - .log_err(); + cx.background_spawn({ + let agent = agent.clone(); + async move { + if let Some(serialized) = + serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() + { + KEY_VALUE_STORE + .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) + .await + .log_err(); + } } }) .detach(); @@ -1051,11 +1121,15 @@ impl AgentPanel { } }; + telemetry::event!("Agent Thread Started", agent = ext_agent.name()); + let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { match ext_agent { - crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { + crate::ExternalAgent::Gemini + | crate::ExternalAgent::NativeAgent + | crate::ExternalAgent::Custom { .. } => { if !cx.has_flag::() { return; } @@ -1071,6 +1145,7 @@ impl AgentPanel { crate::acp::AcpThreadView::new( server, resume_thread, + summarize_thread, workspace.clone(), project, this.acp_history_store.clone(), @@ -1163,7 +1238,7 @@ impl AgentPanel { }); self.set_active_view( ActiveView::prompt_editor( - editor.clone(), + editor, self.history_store.clone(), self.acp_history_store.clone(), self.language_registry.clone(), @@ -1236,7 +1311,7 @@ impl AgentPanel { }); message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } @@ -1405,6 +1480,7 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), + self.project.downgrade(), window, cx, ) @@ -1525,7 +1601,7 @@ impl AgentPanel { return; } - let model = thread_state.configured_model().map(|cm| cm.model.clone()); + let model = thread_state.configured_model().map(|cm| cm.model); if let Some(model) = model { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, cx| { @@ -1680,7 +1756,7 @@ impl AgentPanel { .open_thread_by_id(&id, window, cx) .detach_and_log_err(cx), HistoryEntryId::Context(path) => this - .open_saved_prompt_editor(path.clone(), window, cx) + .open_saved_prompt_editor(path, window, cx) .detach_and_log_err(cx), }) .ok(); @@ -1742,6 +1818,7 @@ impl AgentPanel { agent2::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), + None, window, cx, ), @@ -1782,14 +1859,14 @@ impl AgentPanel { cx: &mut Context, ) { if self.selected_agent != agent { - self.selected_agent = agent; + self.selected_agent = agent.clone(); self.serialize(cx); } self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { - self.selected_agent + self.selected_agent.clone() } pub fn new_agent_thread( @@ -1811,15 +1888,30 @@ impl AgentPanel { AgentType::TextThread => { window.dispatch_action(NewTextThread.boxed_clone(), cx); } - AgentType::NativeAgent => { - self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) - } + AgentType::NativeAgent => self.external_thread( + Some(crate::ExternalAgent::NativeAgent), + None, + None, + window, + cx, + ), AgentType::Gemini => { - self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) - } - AgentType::ClaudeCode => { - self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx) } + AgentType::ClaudeCode => self.external_thread( + Some(crate::ExternalAgent::ClaudeCode), + None, + None, + window, + cx, + ), + AgentType::Custom { name, settings } => self.external_thread( + Some(crate::ExternalAgent::Custom { name, settings }), + None, + None, + window, + cx, + ), } } @@ -1829,7 +1921,13 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + self.external_thread( + Some(ExternalAgent::NativeAgent), + Some(thread), + None, + window, + cx, + ); } } @@ -1966,11 +2064,13 @@ impl AgentPanel { }; match state { - ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) + ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Ready(_) => div() .w_full() @@ -2002,9 +2102,33 @@ impl AgentPanel { } } ActiveView::ExternalAgentThread { thread_view } => { - Label::new(thread_view.read(cx).title(cx)) - .truncate() - .into_any_element() + if let Some(title_editor) = thread_view.read(cx).title_editor() { + div() + .w_full() + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &menu::Confirm, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &editor::actions::Cancel, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .child(title_editor) + .into_any_element() + } else { + Label::new(thread_view.read(cx).title()) + .color(Color::Muted) + .truncate() + .into_any_element() + } } ActiveView::TextThread { title_editor, @@ -2015,6 +2139,7 @@ impl AgentPanel { match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) + .color(Color::Muted) .truncate() .into_any_element(), ContextSummary::Content(summary) => { @@ -2026,6 +2151,7 @@ impl AgentPanel { } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element() } } @@ -2086,6 +2212,8 @@ impl AgentPanel { "Enable Full Screen" }; + let selected_agent = self.selected_agent.clone(); + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) @@ -2106,7 +2234,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); @@ -2166,6 +2293,11 @@ impl AgentPanel { .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); + + if selected_agent == AgentType::Gemini { + menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) + } + menu })) } @@ -2184,7 +2316,6 @@ impl AgentPanel { .trigger_with_tooltip( IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Recent Threads", @@ -2201,6 +2332,8 @@ impl AgentPanel { .menu({ let menu = self.assistant_navigation_menu.clone(); move |window, cx| { + telemetry::event!("View Thread History Clicked"); + if let Some(menu) = menu.as_ref() { menu.update(cx, |_, cx| { cx.defer_in(window, |menu, window, cx| { @@ -2222,8 +2355,6 @@ impl AgentPanel { this.go_back(&workspace::GoBack, window, cx); })) .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) } @@ -2249,7 +2380,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { @@ -2351,8 +2481,10 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } + ActiveView::ExternalAgentThread { thread_view } => { + thread_view.read(cx).as_native_thread(cx) + } + ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -2377,10 +2509,11 @@ impl AgentPanel { .anchor(Corner::TopLeft) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); let workspace = self.workspace.clone(); move |window, cx| { + telemetry::event!("New Thread Clicked"); + let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu @@ -2390,15 +2523,15 @@ impl AgentPanel { let thread = active_thread.read(cx); if !thread.is_empty() { - let thread_id = thread.id().clone(); + let session_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), + Box::new(NewNativeAgentThreadFromSummary { + from_session_id: session_id.clone(), }), cx, ); @@ -2515,13 +2648,64 @@ impl AgentPanel { } }), ) + }) + .when(cx.has_flag::(), |mut menu| { + // Add custom agents from settings + let settings = + agent_servers::AllAgentServersSettings::get_global(cx); + for (agent_name, agent_settings) in &settings.custom { + menu = menu.item( + ContextMenuEntry::new(format!("New {} Thread", agent_name)) + .icon(IconName::Terminal) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + let agent_name = agent_name.clone(); + let agent_settings = agent_settings.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Custom { + name: agent_name + .clone(), + settings: + agent_settings + .clone(), + }, + window, + cx, + ); + }); + } + }); + } + } + }), + ); + } + + menu + }) + .when(cx.has_flag::(), |menu| { + menu.separator().link( + "Add Your Own Agent", + OpenBrowser { + url: "https://agentclientprotocol.com/".into(), + } + .boxed_clone(), + ) }); menu })) } }); - let selected_agent_label = self.selected_agent.label().into(); + let selected_agent_label = self.selected_agent.label(); let selected_agent = div() .id("selected_agent_icon") .when_some(self.selected_agent.icon(), |this, icon| { @@ -3015,7 +3199,7 @@ impl AgentPanel { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); - HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) + HistoryEntryElement::new(entry, cx.entity().downgrade()) .hovered(is_hovered) .on_hover(cx.listener( move |this, is_hovered, _window, cx| { @@ -3106,17 +3290,6 @@ impl AgentPanel { ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::NoProvider => callout.into_any_element(), - ConfigurationError::ProviderPendingTermsAcceptance(provider) => { - Banner::new() - .severity(Severity::Warning) - .child(h_flex().w_full().children( - provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, - cx, - ), - )) - .into_any_element() - } } } @@ -3339,7 +3512,7 @@ impl AgentPanel { .severity(Severity::Error) .icon(IconName::XCircle) .title(header) - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() @@ -3359,7 +3532,7 @@ impl AgentPanel { Callout::new() .severity(Severity::Error) .title("Error") - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() @@ -3606,6 +3779,11 @@ impl Render for AgentPanel { } })) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { + if let Some(thread_view) = this.active_thread_view() { + thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) + } + })) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { @@ -3823,7 +4001,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(message_editor) = panel.active_message_editor() { + if let Some(thread_view) = panel.active_thread_view() { + thread_view.update(cx, |thread_view, cx| { + thread_view.insert_selections(window, cx); + }); + } else if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index a1dbc77084..110c432df3 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,13 +28,14 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; +use agent_servers::AgentServerSettings; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Entity, actions}; +use gpui::{Action, App, Entity, SharedString, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, @@ -128,6 +129,12 @@ actions!( ] ); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)] +#[action(namespace = agent)] +#[action(deprecated_aliases = ["assistant::QuoteSelection"])] +/// Quotes the current selection in the agent panel's message editor. +pub struct QuoteSelection; + /// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] @@ -146,25 +153,50 @@ pub struct NewExternalAgentThread { agent: Option, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] +#[action(namespace = agent)] +#[serde(deny_unknown_fields)] +pub struct NewNativeAgentThreadFromSummary { + from_session_id: agent_client_protocol::SessionId, +} + +// TODO unify this with AgentType +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl ExternalAgent { + fn name(&self) -> &'static str { + match self { + Self::NativeAgent => "zed", + Self::Gemini => "gemini-cli", + Self::ClaudeCode => "claude-code", + Self::Custom { .. } => "custom", + } + } + pub fn server( &self, fs: Arc, history: Entity, ) -> Rc { match self { - ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), - ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Gemini => Rc::new(agent_servers::Gemini), + Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new( + name.clone(), + settings, + )), } } } @@ -240,12 +272,7 @@ pub fn init( client.telemetry().clone(), cx, ); - terminal_inline_assistant::init( - fs.clone(), - prompt_builder.clone(), - client.telemetry().clone(), - cx, - ); + terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -391,7 +418,6 @@ fn register_slash_commands(cx: &mut App) { slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); cx.observe_flag::({ - let slash_command_registry = slash_command_registry.clone(); move |is_enabled, _cx| { if is_enabled { slash_command_registry.register_command( diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index ff5e9362dd..04eb41793f 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1129,7 +1129,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -1196,7 +1196,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1265,7 +1265,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1334,7 +1334,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -1391,7 +1391,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -1473,7 +1473,7 @@ mod tests { } fn simulate_response_stream( - codegen: Entity, + codegen: &Entity, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 0b4568dc87..405b5ed90b 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -818,13 +818,8 @@ pub fn crease_for_mention( let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); - Crease::inline( - range, - placeholder.clone(), - fold_toggle("mention"), - render_trailer, - ) - .with_metadata(CreaseMetadata { icon_path, label }) + Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer) + .with_metadata(CreaseMetadata { icon_path, label }) } fn render_fold_icon_button( diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 747ec46e0a..020d799c79 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -79,8 +79,7 @@ fn search( ) -> Task> { match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -91,8 +90,7 @@ fn search( } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -108,13 +106,8 @@ fn search( .and_then(|t| t.upgrade()) .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - thread_store, - context_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, thread_store, context_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -137,8 +130,7 @@ fn search( Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -196,7 +188,7 @@ fn search( let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); @@ -283,7 +275,7 @@ impl ContextPickerCompletionProvider { ) -> Option { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -330,9 +322,6 @@ impl ContextPickerCompletionProvider { ); let callback = Arc::new({ - let context_store = context_store.clone(); - let selections = selections.clone(); - let selection_infos = selection_infos.clone(); move |_, window: &mut Window, cx: &mut App| { context_store.update(cx, |context_store, cx| { for (buffer, range) in &selections { @@ -441,7 +430,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |window, cx| match &thread_entry { ThreadContextEntry::Thread { id, .. } => { @@ -510,7 +499,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let user_prompt_id = rules.prompt_id; @@ -547,7 +536,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let context_store = context_store.clone(); @@ -704,16 +693,16 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let symbol = symbol.clone(); let context_store = context_store.clone(); let workspace = workspace.clone(); let result = super::symbol_context_picker::add_symbol( - symbol.clone(), + symbol, false, - workspace.clone(), + workspace, context_store.downgrade(), cx, ); @@ -1162,7 +1151,7 @@ mod tests { impl Focusable for AtMentionEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -1480,7 +1469,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::>() } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 90302236fb..13f1234b4d 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1532,13 +1532,11 @@ impl InlineAssistant { .and_then(|item| item.act_as::(cx)) { Some(InlineAssistTarget::Editor(workspace_editor)) - } else if let Some(terminal_view) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - Some(InlineAssistTarget::Terminal(terminal_view)) } else { - None + workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .map(InlineAssistTarget::Terminal) } } } @@ -1693,7 +1691,7 @@ impl InlineAssist { }), range, codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, _subscriptions: vec![ window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 5608143464..a626122769 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1229,27 +1229,27 @@ pub enum GenerationMode { impl GenerationMode { fn start_label(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Generate", + GenerationMode::Generate => "Generate", GenerationMode::Transform => "Transform", } } fn tooltip_interrupt(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Interrupt Generation", + GenerationMode::Generate => "Interrupt Generation", GenerationMode::Transform => "Interrupt Transform", } } fn tooltip_restart(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Restart Generation", + GenerationMode::Generate => "Restart Generation", GenerationMode::Transform => "Restart Transform", } } fn tooltip_accept(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Accept Generation", + GenerationMode::Generate => "Accept Generation", GenerationMode::Transform => "Accept Transform", } } diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 845540979a..aceca79dbf 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, - _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -93,12 +91,11 @@ impl LanguageModelPickerDelegate { let entries = models.entries(); Self { - on_model_changed: on_model_changed.clone(), + on_model_changed, all_models: Arc::new(models), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), - _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } - /// Authenticates all providers in the [`LanguageModelRegistry`]. - /// - /// We do this so that we can populate the language selector with all of the - /// models from the configured providers. - fn authenticate_all_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.spawn(async move |_cx| { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - }) - } - pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } @@ -514,7 +461,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { .pl_0p5() .gap_1p5() .w(px(240.)) - .child(Label::new(model_info.model.name().0.clone()).truncate()), + .child(Label::new(model_info.model.name().0).truncate()), ) .end_slot(div().pr_3().when(is_selected, |this| { this.child( diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index f70d10c1ae..45e7529ec2 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -248,7 +248,7 @@ impl MessageEditor { editor: editor.clone(), project: thread.read(cx).project().clone(), thread, - incompatible_tools_state: incompatible_tools.clone(), + incompatible_tools_state: incompatible_tools, workspace, context_store, prompt_store, @@ -378,18 +378,13 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, provider }) = self + let Some(ConfiguredModel { model, .. }) = self .thread .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) else { return; }; - if provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let creases = extract_message_creases(editor, cx); let text = editor.text(cx); @@ -839,7 +834,6 @@ impl MessageEditor { .child(self.profile_selector.clone()) .child(self.model_selector.clone()) .map({ - let focus_handle = focus_handle.clone(); move |parent| { if is_generating { parent @@ -1683,7 +1677,7 @@ impl Render for MessageEditor { let has_history = self .history_store .as_ref() - .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) + .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok()) .unwrap_or(false) || self .thread @@ -1696,7 +1690,7 @@ impl Render for MessageEditor { !has_history && is_signed_out && has_configured_providers, |this| this.child(cx.new(ApiKeysWithProviders::new)), ) - .when(changed_buffers.len() > 0, |parent| { + .when(!changed_buffers.is_empty(), |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) .child(self.render_editor(window, cx)) @@ -1801,7 +1795,7 @@ impl AgentPreview for MessageEditor { .bg(cx.theme().colors().panel_background) .border_1() .border_color(cx.theme().colors().border) - .child(default_message_editor.clone()) + .child(default_message_editor) .into_any_element(), )]) .into_any_element(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ce25f531e2..f0f53b96b2 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -137,12 +137,11 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); let provider = self.provider.clone(); - let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::(fs.clone(), cx, { let profile_id = profile_id.clone(); move |settings, _cx| { - settings.set_profile(profile_id.clone()); + settings.set_profile(profile_id); } }); @@ -175,7 +174,6 @@ impl Render for ProfileSelector { PopoverMenu::new("profile-selector") .trigger_with_tooltip(trigger_button, { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Profile Menu", diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs index 6b37c5a2d7..87e5d45fe8 100644 --- a/crates/agent_ui/src/slash_command.rs +++ b/crates/agent_ui/src/slash_command.rs @@ -88,8 +88,6 @@ impl SlashCommandCompletionProvider { .map(|(editor, workspace)| { let command_name = mat.string.clone(); let command_range = command_range.clone(); - let editor = editor.clone(); - let workspace = workspace.clone(); Arc::new( move |intent: CompletionIntent, window: &mut Window, @@ -158,7 +156,7 @@ impl SlashCommandCompletionProvider { if let Some(command) = self.slash_commands.command(command_name, cx) { let completions = command.complete_argument( arguments, - new_cancel_flag.clone(), + new_cancel_flag, self.workspace.clone(), window, cx, diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 3859863ebe..e7070c0d7f 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -432,7 +432,7 @@ impl TerminalInlineAssist { terminal: terminal.downgrade(), prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, context_store, prompt_store, _subscriptions: vec![ diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index b3f55ffc43..e9e7eba4b6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,4 +1,5 @@ use crate::{ + QuoteSelection, language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; @@ -89,8 +90,6 @@ actions!( CycleMessageRole, /// Inserts the selected text into the active editor. InsertIntoEditor, - /// Quotes the current selection in the assistant conversation. - QuoteSelection, /// Splits the conversation at the current cursor position. Split, ] @@ -191,7 +190,6 @@ pub struct TextThreadEditor { invoked_slash_command_creases: HashMap, _subscriptions: Vec, last_error: Option, - show_accept_terms: bool, pub(crate) slash_menu_handle: PopoverMenuHandle>, // dragged_file_worktrees is used to keep references to worktrees that were added @@ -290,7 +288,6 @@ impl TextThreadEditor { invoked_slash_command_creases: HashMap::default(), _subscriptions, last_error: None, - show_accept_terms: false, slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), language_model_selector: cx.new(|cx| { @@ -364,24 +361,12 @@ impl TextThreadEditor { if self.sending_disabled(cx) { return; } + telemetry::event!("Agent Message Sent", agent = "zed-text"); self.send_to_model(window, cx); } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let provider = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.provider); - if provider - .as_ref() - .is_some_and(|provider| provider.must_accept_terms(cx)) - { - self.show_accept_terms = true; - cx.notify(); - return; - } - self.last_error = None; - if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { let new_selection = { let cursor = user_message @@ -1739,7 +1724,7 @@ impl TextThreadEditor { render_slash_command_output_toggle, |_, _, _, _| Empty.into_any(), ) - .with_metadata(metadata.crease.clone()) + .with_metadata(metadata.crease) }), cx, ); @@ -1810,7 +1795,7 @@ impl TextThreadEditor { .filter_map(|(anchor, render_image)| { const MAX_HEIGHT_IN_LINES: u32 = 8; let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); - let image = render_image.clone(); + let image = render_image; anchor.is_valid(&buffer).then(|| BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(MAX_HEIGHT_IN_LINES), @@ -1873,7 +1858,7 @@ impl TextThreadEditor { } fn render_send_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx).clone(); + let focus_handle = self.focus_handle(cx); let (style, tooltip) = match token_state(&self.context, cx) { Some(TokenState::NoTokensLeft { .. }) => ( @@ -1931,7 +1916,6 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, - ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } @@ -2015,7 +1999,7 @@ impl TextThreadEditor { None => IconName::Ai, }; - let focus_handle = self.editor().focus_handle(cx).clone(); + let focus_handle = self.editor().focus_handle(cx); PickerPopoverMenu::new( self.language_model_selector.clone(), diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index beeaf0c43b..ada973cddf 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,13 +2,13 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; -// mod new_thread_button; mod onboarding_modal; pub mod preview; +mod unavailable_editing_tooltip; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; -// pub use new_thread_button::*; pub use onboarding_modal::*; +pub use unavailable_editing_tooltip::*; diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 4e33e151cd..7c7fbd27f0 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -499,7 +499,7 @@ impl AddedContext { let thread = handle.thread.clone(); Some(Rc::new(move |_, cx| { let text = thread.read(cx).latest_detailed_summary_or_text(); - ContextPillHover::new_text(text.clone(), cx).into() + ContextPillHover::new_text(text, cx).into() })) }, handle: AgentContextHandle::Thread(handle), @@ -574,7 +574,7 @@ impl AddedContext { .unwrap_or_else(|| "Unnamed Rule".into()); Some(AddedContext { kind: ContextKind::Rules, - name: title.clone(), + name: title, parent: None, tooltip: None, icon_path: None, diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs deleted file mode 100644 index 347d6adcaf..0000000000 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ /dev/null @@ -1,75 +0,0 @@ -use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct NewThreadButton { - id: ElementId, - label: SharedString, - icon: IconName, - keybinding: Option, - on_click: Option>, -} - -impl NewThreadButton { - fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { - Self { - id: id.into(), - label: label.into(), - icon, - keybinding: None, - on_click: None, - } - } - - fn keybinding(mut self, keybinding: Option) -> Self { - self.keybinding = keybinding; - self - } - - fn on_click(mut self, handler: F) -> Self - where - F: Fn(&mut Window, &mut App) + 'static, - { - self.on_click = Some(Box::new( - move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx), - )); - self - } -} - -impl RenderOnce for NewThreadButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .w_full() - .py_1p5() - .px_2() - .gap_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border.opacity(0.4)) - .bg(cx.theme().colors().element_active.opacity(0.2)) - .hover(|style| { - style - .bg(cx.theme().colors().element_hover) - .border_color(cx.theme().colors().border) - }) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(self.icon) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new(self.label).size(LabelSize::Small)), - ) - .when_some(self.keybinding, |this, keybinding| { - this.child(keybinding.size(rems_from_px(10.))) - }) - .when_some(self.on_click, |this, on_click| { - this.on_click(move |event, window, cx| on_click(event, window, cx)) - }) - } -} diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index 29b12ea627..d4d037b976 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -86,23 +86,18 @@ impl RenderOnce for UsageCallout { (IconName::Warning, Severity::Warning) }; - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .severity(severity) - .icon(icon) - .title(title) - .description(message) - .actions_slot( - Button::new("upgrade", button_text) - .label_size(LabelSize::Small) - .on_click(move |_, _, cx| { - cx.open_url(&url); - }), - ), + Callout::new() + .icon(icon) + .severity(severity) + .icon(icon) + .title(title) + .description(message) + .actions_slot( + Button::new("upgrade", button_text) + .label_size(LabelSize::Small) + .on_click(move |_, _, cx| { + cx.open_url(&url); + }), ) .into_any_element() } diff --git a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs new file mode 100644 index 0000000000..78d4c64e0a --- /dev/null +++ b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs @@ -0,0 +1,29 @@ +use gpui::{Context, IntoElement, Render, Window}; +use ui::{prelude::*, tooltip_container}; + +pub struct UnavailableEditingTooltip { + agent_name: SharedString, +} + +impl UnavailableEditingTooltip { + pub fn new(agent_name: SharedString) -> Self { + Self { agent_name } + } +} + +impl Render for UnavailableEditingTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + tooltip_container(window, cx, |this, _, _| { + this.child(Label::new("Unavailable Editing")).child( + div().max_w_64().child( + Label::new(format!( + "Editing previous messages is not available for {} yet.", + self.agent_name + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 0a34a29068..fadc4222ae 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -33,7 +33,7 @@ impl ApiKeysWithProviders { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 23810b74f3..77f41d1a73 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -50,7 +50,7 @@ impl AgentPanelOnboarding { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } @@ -74,7 +74,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 { + if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 717abebfd1..6d8ac64725 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; +use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -43,12 +43,10 @@ impl From for SignInStatus { #[derive(RegisterComponent, IntoElement)] pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, - pub has_accepted_terms_of_service: bool, pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, - pub accept_terms_of_service: Arc, pub dismiss_onboarding: Option>, } @@ -64,17 +62,9 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), - has_accepted_terms_of_service: store.has_accepted_terms_of_service(), plan: store.plan(), account_too_young: store.account_too_young(), continue_with_zed_ai, - accept_terms_of_service: Arc::new({ - let store = user_store.clone(); - move |_window, cx| { - let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx)); - task.detach_and_log_err(cx); - } - }), sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); @@ -94,42 +84,6 @@ impl ZedAiOnboarding { self } - fn render_accept_terms_of_service(&self) -> AnyElement { - v_flex() - .gap_1() - .w_full() - .child(Headline::new("Accept Terms of Service")) - .child( - Label::new("We don’t sell your data, track you across the web, or compromise your privacy.") - .color(Color::Muted) - .mb_2(), - ) - .child( - Button::new("terms_of_service", "Review Terms of Service") - .full_width() - .style(ButtonStyle::Outlined) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - telemetry::event!("Review Terms of Service Clicked"); - cx.open_url(&zed_urls::terms_of_service(cx)) - }), - ) - .child( - Button::new("accept_terms", "Accept") - .full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click({ - let callback = self.accept_terms_of_service.clone(); - move |_, window, cx| { - telemetry::event!("Terms of Service Accepted"); - (callback)(window, cx)} - }), - ) - .into_any_element() - } - fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); let plan_definitions = PlanDefinitions; @@ -359,14 +313,10 @@ impl ZedAiOnboarding { impl RenderOnce for ZedAiOnboarding { fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement { if matches!(self.sign_in_status, SignInStatus::SignedIn) { - if self.has_accepted_terms_of_service { - match self.plan { - None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), - Some(Plan::ZedProTrial) => self.render_trial_state(cx), - Some(Plan::ZedPro) => self.render_pro_plan_state(cx), - } - } else { - self.render_accept_terms_of_service() + match self.plan { + None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(cx), } } else { self.render_sign_in_disclaimer(cx) @@ -390,18 +340,15 @@ impl Component for ZedAiOnboarding { fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn onboarding( sign_in_status: SignInStatus, - has_accepted_terms_of_service: bool, plan: Option, account_too_young: bool, ) -> AnyElement { ZedAiOnboarding { sign_in_status, - has_accepted_terms_of_service, plan, account_too_young, continue_with_zed_ai: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}), - accept_terms_of_service: Arc::new(|_, _| {}), dismiss_onboarding: None, } .into_any_element() @@ -415,27 +362,23 @@ impl Component for ZedAiOnboarding { .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, false, None, false), - ), - single_example( - "Not Accepted ToS", - onboarding(SignInStatus::SignedIn, false, None, false), + onboarding(SignInStatus::SignedOut, None, false), ), single_example( "Young Account", - onboarding(SignInStatus::SignedIn, true, None, true), + onboarding(SignInStatus::SignedIn, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), ), ]) .into_any_element(), diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index e9639ca075..106dcb0aef 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions} #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - pub sign_in_status: SignInStatus, - pub sign_in: Arc, - pub account_too_young: bool, - pub user_plan: Option, - pub tab_index: Option, + sign_in_status: SignInStatus, + sign_in: Arc, + account_too_young: bool, + user_plan: Option, + tab_index: Option, } impl AiUpsellCard { @@ -43,6 +43,11 @@ impl AiUpsellCard { tab_index: None, } } + + pub fn tab_index(mut self, tab_index: Option) -> Self { + self.tab_index = tab_index; + self + } } impl RenderOnce for AiUpsellCard { diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 4d0bfae444..12eda0954a 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -2282,7 +2282,7 @@ impl AssistantContext { let mut contents = self.contents(cx).peekable(); fn collect_text_content(buffer: &Buffer, range: Range) -> Option { - let text: String = buffer.text_for_range(range.clone()).collect(); + let text: String = buffer.text_for_range(range).collect(); if text.trim().is_empty() { None } else { diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 28cc8ef8f0..61d748cbdd 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1321,7 +1321,7 @@ fn test_summarize_error( fn setup_context_editor_with_fake_model( cx: &mut TestAppContext, ) -> (Entity, Arc) { - let registry = Arc::new(LanguageRegistry::test(cx.executor().clone())); + let registry = Arc::new(LanguageRegistry::test(cx.executor())); let fake_provider = Arc::new(FakeLanguageModelProvider::default()); let fake_model = Arc::new(fake_provider.test_model()); @@ -1376,7 +1376,7 @@ fn messages_cache( context .read(cx) .messages(cx) - .map(|message| (message.id, message.cache.clone())) + .map(|message| (message.id, message.cache)) .collect() } @@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand { sections: vec![], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6d13531a57..6960d9db79 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -862,7 +862,7 @@ impl ContextStore { ContextServerStatus::Running => { self.load_context_server_slash_commands( server_id.clone(), - context_server_store.clone(), + context_server_store, cx, ); } diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 828f115bf5..4b85fa2edf 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -161,7 +161,7 @@ impl SlashCommandOutput { } /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. - pub fn to_event_stream(mut self) -> BoxStream<'static, Result> { + pub fn into_event_stream(mut self) -> BoxStream<'static, Result> { self.ensure_valid_section_ranges(); let mut events = Vec::new(); @@ -363,7 +363,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::>().await; + let events = output.clone().into_event_stream().collect::>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -386,7 +386,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -415,7 +415,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::>().await; + let events = output.clone().into_event_stream().collect::>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -452,7 +452,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -493,7 +493,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::>().await; + let events = output.clone().into_event_stream().collect::>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -562,7 +562,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index 74c46ffb5f..e47ae52c98 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -166,7 +166,7 @@ impl SlashCommand for ExtensionSlashCommand { .collect(), run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs index 8b088ea012..d58b2edc4c 100644 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ b/crates/assistant_slash_commands/src/cargo_workspace_command.rs @@ -150,7 +150,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 6caa1beb3b..ee0cbf54c2 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -191,7 +191,7 @@ impl SlashCommand for ContextServerSlashCommand { text: prompt, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs index 6fce7f07a4..01eff881cf 100644 --- a/crates/assistant_slash_commands/src/default_command.rs +++ b/crates/assistant_slash_commands/src/default_command.rs @@ -85,7 +85,7 @@ impl SlashCommand for DefaultSlashCommand { text, run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 2cc4591386..ea05fca588 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -118,7 +118,7 @@ impl SlashCommand for DeltaSlashCommand { } anyhow::ensure!(changes_detected, "no new changes detected"); - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 536fe9f0ef..8b1dbd515c 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand { score: 0., positions: Vec::new(), worktree_id: entry.worktree_id.to_usize(), - path: entry.path.clone(), + path: entry.path, path_prefix: path_prefix.clone(), is_dir: false, // Diagnostics can't be produced for directories distance_to_relative_ancestor: 0, @@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand { window.spawn(cx, async move |_| { task.await? - .map(|output| output.to_event_stream()) + .map(|output| output.into_event_stream()) .context("No diagnostics found") }) } diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index 4e0bb3d05a..6d3f66c9a2 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -177,7 +177,7 @@ impl SlashCommand for FetchSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index 894aa94a27..a973d653e4 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -371,7 +371,7 @@ fn collect_files( &mut output, ) .log_err(); - let mut buffer_events = output.to_event_stream(); + let mut buffer_events = output.into_event_stream(); while let Some(event) = buffer_events.next().await { events_tx.unbounded_send(event)?; } diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs index e4abef2a7c..aec21e7173 100644 --- a/crates/assistant_slash_commands/src/now_command.rs +++ b/crates/assistant_slash_commands/src/now_command.rs @@ -66,6 +66,6 @@ impl SlashCommand for NowSlashCommand { }], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index c177f9f359..bbd6d3e3ad 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand { }; let store = PromptStore::global(cx); - let title = SharedString::from(title.clone()); + let title = SharedString::from(title); let prompt = cx.spawn({ let title = title.clone(); async move |cx| { @@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand { }], run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs index ef93146431..3028709144 100644 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ b/crates/assistant_slash_commands/src/symbols_command.rs @@ -92,7 +92,7 @@ impl SlashCommand for OutlineSlashCommand { text: outline_text, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index e4ae391a9c..a124beed63 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -157,7 +157,7 @@ impl SlashCommand for TabSlashCommand { for (full_path, buffer, _) in tab_items_search.await? { append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err(); } - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index ea2fa02663..4f182b3148 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1153,8 +1153,7 @@ impl EvalInput { .expect("Conversation must end with an edit_file tool use") .clone(); - let edit_file_input: EditFileToolInput = - serde_json::from_value(tool_use.input.clone()).unwrap(); + let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap(); EvalInput { conversation, @@ -1460,7 +1459,7 @@ impl EditAgentTest { async fn new(cx: &mut TestAppContext) -> Self { cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); cx.update(|cx| { settings::init(cx); gpui_tokio::init(cx); @@ -1475,7 +1474,7 @@ impl EditAgentTest { Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); crate::init(client.http_client(), cx); }); diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 092bdce8b3..33b37679f0 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -319,7 +319,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!(push(&mut finder, ""), None); assert_eq!(finish(finder), None); } @@ -333,7 +333,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push partial query assert_eq!(push(&mut finder, "This"), None); @@ -365,7 +365,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push a fuzzy query that should match the first function assert_eq!( @@ -391,7 +391,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // No match initially assert_eq!(push(&mut finder, "Lin"), None); @@ -420,7 +420,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push text in small chunks across line boundaries assert_eq!(push(&mut finder, "jumps "), None); // No newline yet @@ -458,7 +458,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!( push(&mut finder, "impl Debug for User {\n"), @@ -711,7 +711,7 @@ mod tests { "Expected to match `second_function` based on the line hint" ); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); matcher.push(query, None); matcher.finish(); let best_match = matcher.select_best_match(); @@ -727,7 +727,7 @@ mod tests { let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone()); let snapshot = buffer.snapshot(); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); // Split query into random chunks let chunks = to_random_chunks(rng, query); @@ -794,10 +794,8 @@ mod tests { fn finish(mut finder: StreamingFuzzyMatcher) -> Option { let snapshot = finder.snapshot.clone(); let matches = finder.finish(); - if let Some(range) = matches.first() { - Some(snapshot.text_for_range(range.clone()).collect::()) - } else { - None - } + matches + .first() + .map(|range| snapshot.text_for_range(range.clone()).collect::()) } } diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 33d08b4f88..95b01c40eb 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -376,7 +376,7 @@ impl Tool for EditFileTool { let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), - new_text: new_text.clone(), + new_text, old_text, raw_output: Some(agent_output), }; @@ -643,7 +643,7 @@ impl EditFileToolCard { diff }); - self.buffer = Some(buffer.clone()); + self.buffer = Some(buffer); self.base_text = Some(base_text.into()); self.buffer_diff = Some(buffer_diff.clone()); @@ -776,7 +776,6 @@ impl EditFileToolCard { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, &language_registry, cx).await } @@ -863,7 +862,6 @@ impl ToolCard for EditFileToolCard { ) .on_click({ let path = self.path.clone(); - let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, { diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index 79e205f205..cc22c9fc09 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -118,7 +118,7 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false + true } fn may_perform_edits(&self) -> bool { diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 6b62638a4c..d1451132ae 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -234,7 +234,7 @@ impl ToolCard for FindPathToolCard { workspace: WeakEntity, cx: &mut Context, ) -> impl IntoElement { - let matches_label: SharedString = if self.paths.len() == 0 { + let matches_label: SharedString = if self.paths.is_empty() { "No matches".into() } else if self.paths.len() == 1 { "1 match".into() @@ -435,8 +435,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); @@ -447,8 +447,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); } diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 1dd74b99e7..41dde5bbfe 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -327,7 +327,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -415,7 +415,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -494,7 +494,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 766ee3b161..a6e984fca6 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -68,7 +68,7 @@ impl Tool for ReadFileTool { } fn icon(&self) -> IconName { - IconName::ToolRead + IconName::ToolSearch } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 358d62ee1a..b28e55e78a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -350,7 +350,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs index b71453373f..b41f19432f 100644 --- a/crates/assistant_tools/src/ui/tool_call_card_header.rs +++ b/crates/assistant_tools/src/ui/tool_call_card_header.rs @@ -101,14 +101,11 @@ impl RenderOnce for ToolCallCardHeader { }) .when_some(secondary_text, |this, secondary_text| { this.child(bullet_divider()) - .child(div().text_size(font_size).child(secondary_text.clone())) + .child(div().text_size(font_size).child(secondary_text)) }) .when_some(code_path, |this, code_path| { - this.child(bullet_divider()).child( - Label::new(code_path.clone()) - .size(LabelSize::Small) - .inline_code(cx), - ) + this.child(bullet_divider()) + .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx)) }) .with_animation( "loading-label", diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 47a6958b7a..dbcca0a1f6 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -193,10 +193,7 @@ impl ToolCard for WebSearchToolCard { ) } }) - .on_click({ - let url = url.clone(); - move |_, _, cx| cx.open_url(&url) - }) + .on_click(move |_, _, cx| cx.open_url(&url)) })) .into_any(), ), diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 5146396b92..ae7eb52fd3 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -15,9 +15,10 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true -derive_more.workspace = true gpui.workspace = true -parking_lot.workspace = true +settings.workspace = true +schemars.workspace = true +serde.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs deleted file mode 100644 index fd5c935d87..0000000000 --- a/crates/audio/src/assets.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{io::Cursor, sync::Arc}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use gpui::{App, AssetSource, Global}; -use rodio::{Decoder, Source, source::Buffered}; - -type Sound = Buffered>>>; - -pub struct SoundRegistry { - cache: Arc>>, - assets: Box, -} - -struct GlobalSoundRegistry(Arc); - -impl Global for GlobalSoundRegistry {} - -impl SoundRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - cache: Default::default(), - assets: Box::new(source), - }) - } - - pub fn global(cx: &App) -> Arc { - cx.global::().0.clone() - } - - pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) { - cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source))); - } - - pub fn get(&self, name: &str) -> Result + use<>> { - if let Some(wav) = self.cache.lock().get(name) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", name); - let bytes = self - .assets - .load(&path)? - .map(anyhow::Ok) - .with_context(|| format!("No asset available for path {path}"))?? - .into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.buffered(); - - self.cache.lock().insert(name.to_string(), source.clone()); - - Ok(source) - } -} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 44baa16aa2..b4f2c24fef 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,16 +1,19 @@ -use assets::SoundRegistry; -use derive_more::{Deref, DerefMut}; -use gpui::{App, AssetSource, BorrowAppContext, Global}; -use rodio::{OutputStream, OutputStreamBuilder}; +use anyhow::{Context as _, Result, anyhow}; +use collections::HashMap; +use gpui::{App, BorrowAppContext, Global}; +use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; +use settings::Settings; +use std::io::Cursor; use util::ResultExt; -mod assets; +mod audio_settings; +pub use audio_settings::AudioSettings; -pub fn init(source: impl AssetSource, cx: &mut App) { - SoundRegistry::set_global(source, cx); - cx.set_global(GlobalAudio(Audio::new())); +pub fn init(cx: &mut App) { + AudioSettings::register(cx); } +#[derive(Copy, Clone, Eq, Hash, PartialEq)] pub enum Sound { Joined, Leave, @@ -38,18 +41,12 @@ impl Sound { #[derive(Default)] pub struct Audio { output_handle: Option, + source_cache: HashMap>>>>, } -#[derive(Deref, DerefMut)] -struct GlobalAudio(Audio); - -impl Global for GlobalAudio {} +impl Global for Audio {} impl Audio { - pub fn new() -> Self { - Self::default() - } - fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); @@ -58,26 +55,51 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_sound(sound: Sound, cx: &mut App) { - if !cx.has_global::() { - return; - } + pub fn play_source( + source: impl rodio::Source + Send + 'static, + cx: &mut App, + ) -> anyhow::Result<()> { + cx.update_default_global(|this: &mut Self, _cx| { + let output_handle = this + .ensure_output_exists() + .ok_or_else(|| anyhow!("Could not open audio output"))?; + output_handle.mixer().add(source); + Ok(()) + }) + } - cx.update_global::(|this, cx| { + pub fn play_sound(sound: Sound, cx: &mut App) { + cx.update_default_global(|this: &mut Self, cx| { + let source = this.sound_source(sound, cx).log_err()?; let output_handle = this.ensure_output_exists()?; - let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.mixer().add(source); Some(()) }); } pub fn end_call(cx: &mut App) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, _| { + cx.update_default_global(|this: &mut Self, _cx| { this.output_handle.take(); }); } + + fn sound_source(&mut self, sound: Sound, cx: &App) -> Result> { + if let Some(wav) = self.source_cache.get(&sound) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", sound.file()); + let bytes = cx + .asset_source() + .load(&path)? + .map(anyhow::Ok) + .with_context(|| format!("No asset available for path {path}"))?? + .into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.buffered(); + + self.source_cache.insert(sound, source.clone()); + + Ok(source) + } } diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs new file mode 100644 index 0000000000..807179881c --- /dev/null +++ b/crates/audio/src/audio_settings.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use gpui::App; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +#[derive(Deserialize, Debug)] +pub struct AudioSettings { + /// Opt into the new audio system. + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, // default is false +} + +/// Configuration of audio in Zed. +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[serde(default)] +pub struct AudioSettingsContent { + /// Whether to use the experimental audio system + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, +} + +impl Settings for AudioSettings { + const KEY: Option<&'static str> = Some("audio"); + + type FileContent = AudioSettingsContent; + + fn load(sources: SettingsSources, _cx: &mut App) -> Result { + sources.json_merge() + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs index 3aa57094d3..21ead701b2 100644 --- a/crates/auto_update_helper/src/auto_update_helper.rs +++ b/crates/auto_update_helper/src/auto_update_helper.rs @@ -128,23 +128,20 @@ mod windows_impl { #[test] fn test_parse_args() { // launch can be specified via two separate arguments - assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true); - assert_eq!( - parse_args(["--launch".into(), "false".into()]).launch, - false - ); + assert!(parse_args(["--launch".into(), "true".into()]).launch); + assert!(!parse_args(["--launch".into(), "false".into()]).launch); // launch can be specified via one single argument - assert_eq!(parse_args(["--launch=true".into()]).launch, true); - assert_eq!(parse_args(["--launch=false".into()]).launch, false); + assert!(parse_args(["--launch=true".into()]).launch); + assert!(!parse_args(["--launch=false".into()]).launch); // launch defaults to true on no arguments - assert_eq!(parse_args([]).launch, true); + assert!(parse_args([]).launch); // launch defaults to true on invalid arguments - assert_eq!(parse_args(["--launch".into()]).launch, true); - assert_eq!(parse_args(["--launch=".into()]).launch, true); - assert_eq!(parse_args(["--launch=invalid".into()]).launch, true); + assert!(parse_args(["--launch".into()]).launch); + assert!(parse_args(["--launch=".into()]).launch); + assert!(parse_args(["--launch=invalid".into()]).launch); } } } diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 920f8d5fcf..7627716176 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -90,11 +90,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { @@ -105,11 +101,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 63baef1f7d..7063dffd6d 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -114,7 +114,7 @@ fn view_release_notes_locally( cx, ); workspace.add_item_to_active_pane( - Box::new(markdown_preview.clone()), + Box::new(markdown_preview), None, true, window, diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6b38fe5576..b20dad4ebb 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -162,6 +162,22 @@ impl BufferDiffSnapshot { } } + fn unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> BufferDiffSnapshot { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiffSnapshot { + inner: BufferDiffInner { + base_text, + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text_exists: false, + }, + secondary_diff: None, + } + } + fn new_with_base_text( buffer: text::BufferSnapshot, base_text: Option>, @@ -175,12 +191,8 @@ impl BufferDiffSnapshot { if let Some(text) = &base_text { let base_text_rope = Rope::from(text.as_str()); base_text_pair = Some((text.clone(), base_text_rope.clone())); - let snapshot = language::Buffer::build_snapshot( - base_text_rope, - language.clone(), - language_registry.clone(), - cx, - ); + let snapshot = + language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); base_text_snapshot = cx.background_spawn(snapshot); base_text_exists = true; } else { @@ -217,7 +229,10 @@ impl BufferDiffSnapshot { cx: &App, ) -> impl Future + use<> { let base_text_exists = base_text.is_some(); - let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); + let base_text_pair = base_text.map(|text| { + debug_assert_eq!(&*text, &base_text_snapshot.text()); + (text, base_text_snapshot.as_rope().clone()) + }); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { @@ -877,6 +892,18 @@ impl BufferDiff { } } + pub fn new_unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> Self { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiff { + buffer_id: buffer.remote_id(), + inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner, + secondary_diff: None, + } + } + #[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: &str, @@ -957,7 +984,7 @@ impl BufferDiff { .buffer_range .start; let end = self - .hunks_intersecting_range_rev(range.clone(), buffer) + .hunks_intersecting_range_rev(range, buffer) .next()? .buffer_range .end; @@ -1441,7 +1468,7 @@ mod tests { .unindent(); let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); - let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx); + let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); let mut uncommitted_diff = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff)); @@ -2028,8 +2055,8 @@ mod tests { fn gen_working_copy(rng: &mut StdRng, head: &str) -> String { let mut old_lines = { let mut old_lines = Vec::new(); - let mut old_lines_iter = head.lines(); - while let Some(line) = old_lines_iter.next() { + let old_lines_iter = head.lines(); + for line in old_lines_iter { assert!(!line.ends_with("\n")); old_lines.push(line.to_owned()); } @@ -2133,7 +2160,7 @@ mod tests { diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx) .collect::>() }); - if hunks.len() == 0 { + if hunks.is_empty() { return; } diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 943e819ad6..828248b330 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -135,7 +135,7 @@ impl ChannelBuffer { } } - for (_, old_collaborator) in &self.collaborators { + for old_collaborator in self.collaborators.values() { if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id, cx) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 850a494613..daa8a91c7c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1073,7 +1073,7 @@ impl ChannelStore { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { - for (_, buffer) in &this.opened_buffers { + for buffer in this.opened_buffers.values() { if let OpenEntityHandle::Open(buffer) = &buffer && let Some(buffer) = buffer.upgrade() { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index c92226eeeb..2a91433084 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -438,7 +438,7 @@ fn init_test(cx: &mut App) -> Entity { let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); - let client = Client::new(clock, http.clone(), cx); + let client = Client::new(clock, http, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); client::init(&client, cx); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 925d5ddefb..b84e7a9f7a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -926,7 +926,7 @@ mod mac_os { fn path(&self) -> PathBuf { match self { - Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(), + Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"), Bundle::LocalPath { executable, .. } => executable.clone(), } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 058a12417a..2bbe7dd1b5 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock> = LazyLock::new(|| { .and_then(|s| if s.is_empty() { None } else { Some(s) }) }); +pub static USE_WEB_LOGIN: LazyLock = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok()); + pub static ADMIN_API_TOKEN: LazyLock> = LazyLock::new(|| { std::env::var("ZED_ADMIN_API_TOKEN") .ok() @@ -181,7 +183,7 @@ pub fn init(client: &Arc, cx: &mut App) { }); cx.on_action({ - let client = client.clone(); + let client = client; move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { @@ -791,7 +793,7 @@ impl Client { Arc::new(move |subscriber, envelope, client, cx| { let subscriber = subscriber.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - handler(subscriber, *envelope, client.clone(), cx).boxed_local() + handler(subscriber, *envelope, client, cx).boxed_local() }), ); if prev_handler.is_some() { @@ -1029,11 +1031,11 @@ impl Client { Status::SignedOut | Status::Authenticated => true, Status::ConnectionError | Status::ConnectionLost - | Status::Authenticating { .. } + | Status::Authenticating | Status::AuthenticationError - | Status::Reauthenticating { .. } + | Status::Reauthenticating | Status::ReconnectionError { .. } => false, - Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { + Status::Connected { .. } | Status::Connecting | Status::Reconnecting => { return ConnectionResult::Result(Ok(())); } Status::UpgradeRequired => { @@ -1290,19 +1292,21 @@ impl Client { "http" => Http, _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?, }; - let rpc_host = rpc_url - .host_str() - .zip(rpc_url.port_or_known_default()) - .context("missing host in rpc url")?; - let stream = { - let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap(); - let _guard = handle.enter(); - match proxy { - Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, - None => Box::new(TcpStream::connect(rpc_host).await?), + let stream = gpui_tokio::Tokio::spawn_result(cx, { + let rpc_url = rpc_url.clone(); + async move { + let rpc_host = rpc_url + .host_str() + .zip(rpc_url.port_or_known_default()) + .context("missing host in rpc url")?; + Ok(match proxy { + Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, + None => Box::new(TcpStream::connect(rpc_host).await?), + }) } - }; + })? + .await?; log::info!("connected to rpc endpoint {}", rpc_url); @@ -1390,11 +1394,13 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - eprintln!("authenticate as admin {login}, {token}"); + if !*USE_WEB_LOGIN { + eprintln!("authenticate as admin {login}, {token}"); - return this - .authenticate_as_admin(http, login.clone(), token.clone()) - .await; + return this + .authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } } // Start an HTTP server to receive the redirect from Zed's sign-in page. @@ -1902,10 +1908,7 @@ mod tests { assert!(matches!(status.next().await, Some(Status::Connecting))); executor.advance_clock(CONNECTION_TIMEOUT); - assert!(matches!( - status.next().await, - Some(Status::ConnectionError { .. }) - )); + assert!(matches!(status.next().await, Some(Status::ConnectionError))); auth_and_connect.await.into_response().unwrap_err(); // Allow the connection to be established. @@ -1929,10 +1932,7 @@ mod tests { }) }); executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); - assert!(matches!( - status.next().await, - Some(Status::Reconnecting { .. }) - )); + assert!(matches!(status.next().await, Some(Status::Reconnecting))); executor.advance_clock(CONNECTION_TIMEOUT); assert!(matches!( @@ -2048,10 +2048,7 @@ mod tests { assert_eq!(*auth_count.lock(), 1); assert_eq!(*dropped_auth_count.lock(), 0); - let _authenticate = cx.spawn({ - let client = client.clone(); - |cx| async move { client.connect(false, &cx).await } - }); + let _authenticate = cx.spawn(|cx| async move { client.connect(false, &cx).await }); executor.run_until_parked(); assert_eq!(*auth_count.lock(), 2); assert_eq!(*dropped_auth_count.lock(), 1); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 54b3d3f801..a5c1532c75 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { option_env!("ZED_MINIDUMP_ENDPOINT") - .map(|s| s.to_owned()) + .map(str::to_string) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); @@ -739,7 +739,7 @@ mod tests { ); // Third scan of worktree does not double report, as we already reported - test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id); + test_project_discovery_helper(telemetry, vec!["package.json"], None, worktree_id); } #[gpui::test] @@ -751,7 +751,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "pnpm-lock.yaml"], Some(vec!["node", "pnpm"]), 1, @@ -767,7 +767,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "yarn.lock"], Some(vec!["node", "yarn"]), 1, @@ -786,7 +786,7 @@ mod tests { // project type for the same worktree multiple times test_project_discovery_helper( - telemetry.clone().clone(), + telemetry.clone(), vec!["global.json"], Some(vec!["dotnet"]), 1, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 2599be9b16..d23eb37519 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{Client, Status, TypedEnvelope, proto}; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result}; use chrono::{DateTime, Utc}; use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; @@ -41,16 +41,11 @@ impl std::fmt::Display for ChannelId { pub struct ProjectId(pub u64); impl ProjectId { - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 } } -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub struct DevServerProjectId(pub u64); - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); @@ -116,7 +111,6 @@ pub struct UserStore { edit_prediction_usage: Option, plan_info: Option, current_user: watch::Receiver>>, - accepted_tos_at: Option>, contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, @@ -194,7 +188,6 @@ impl UserStore { plan_info: None, model_request_usage: None, edit_prediction_usage: None, - accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), participant_indices: Default::default(), @@ -271,7 +264,6 @@ impl UserStore { Status::SignedOut => { current_user_tx.send(None).await.ok(); this.update(cx, |this, cx| { - this.accepted_tos_at = None; cx.emit(Event::PrivateUserInfoUpdated); cx.notify(); this.clear_contacts() @@ -791,19 +783,6 @@ impl UserStore { .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); } - let accepted_tos_at = { - #[cfg(debug_assertions)] - if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() { - None - } else { - response.user.accepted_tos_at - } - - #[cfg(not(debug_assertions))] - response.user.accepted_tos_at - }; - - self.accepted_tos_at = Some(accepted_tos_at); self.model_request_usage = Some(ModelRequestUsage(RequestUsage { limit: response.plan.usage.model_requests.limit, amount: response.plan.usage.model_requests.used as i32, @@ -846,32 +825,6 @@ impl UserStore { self.current_user.clone() } - pub fn has_accepted_terms_of_service(&self) -> bool { - self.accepted_tos_at - .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) - } - - pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { - if self.current_user().is_none() { - return Task::ready(Err(anyhow!("no current user"))); - }; - - let client = self.client.clone(); - cx.spawn(async move |this, cx| -> anyhow::Result<()> { - let client = client.upgrade().context("client not found")?; - let response = client - .cloud_client() - .accept_terms_of_service() - .await - .context("error accepting tos")?; - this.update(cx, |this, cx| { - this.accepted_tos_at = Some(response.user.accepted_tos_at); - cx.emit(Event::PrivateUserInfoUpdated); - })?; - Ok(()) - }) - } - fn load_users( &self, request: impl RequestMessage, diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 92417d8319..7fd96fcef0 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -102,13 +102,7 @@ impl CloudApiClient { let credentials = credentials.as_ref().context("no credentials provided")?; let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); - Ok(cx.spawn(async move |cx| { - let handle = cx - .update(|cx| Tokio::handle(cx)) - .ok() - .context("failed to get Tokio handle")?; - let _guard = handle.enter(); - + Ok(Tokio::spawn_result(cx, async move { let ws = WebSocket::connect(connect_url) .with_request( request::Builder::new() @@ -121,34 +115,6 @@ impl CloudApiClient { })) } - pub async fn accept_terms_of_service(&self) -> Result { - let request = self.build_request( - Request::builder().method(Method::POST).uri( - self.http_client - .build_zed_cloud_url("/client/terms_of_service/accept", &[])? - .as_ref(), - ), - AsyncBody::default(), - )?; - - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - Ok(serde_json::from_str(&body)?) - } - pub async fn create_llm_token( &self, system_id: Option, diff --git a/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql new file mode 100644 index 0000000000..89a42ab82b --- /dev/null +++ b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql @@ -0,0 +1,4 @@ +alter table billing_subscriptions + add column orb_subscription_status text, + add column orb_current_billing_period_start_date timestamp without time zone, + add column orb_current_billing_period_end_date timestamp without time zone; diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index c500872fd7..da78a98069 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -280,7 +280,7 @@ pub async fn post_hang( service = "client", version = %report.app_version.unwrap_or_default().to_string(), os_name = %report.os_name, - os_version = report.os_version.unwrap_or_default().to_string(), + os_version = report.os_version.unwrap_or_default(), incident_id = %incident_id, installation_id = %report.installation_id.unwrap_or_default(), backtrace = %backtrace, diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 5a2a1329bb..e484d6b510 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -236,7 +236,7 @@ mod test { #[gpui::test] async fn test_verify_access_token(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); + let test_db = crate::db::TestDb::sqlite(cx.executor()); let db = test_db.db(); let user = db diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 774eec5d2c..95a485305c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -685,7 +685,7 @@ impl LocalSettingsKind { } } - pub fn to_proto(&self) -> proto::LocalSettingsKind { + pub fn to_proto(self) -> proto::LocalSettingsKind { match self { Self::Settings => proto::LocalSettingsKind::Settings, Self::Tasks => proto::LocalSettingsKind::Tasks, diff --git a/crates/collab/src/db/tests/embedding_tests.rs b/crates/collab/src/db/tests/embedding_tests.rs index 367e89f87b..5d8d69c030 100644 --- a/crates/collab/src/db/tests/embedding_tests.rs +++ b/crates/collab/src/db/tests/embedding_tests.rs @@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime, PrimitiveDateTime}; // SQLite does not support array arguments, so we only test this against a real postgres instance #[gpui::test] async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let provider = "test_model"; @@ -38,7 +38,7 @@ async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let model = "test_model"; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01f553edf2..73f327166a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -310,7 +310,7 @@ impl Server { let mut server = Self { id: parking_lot::Mutex::new(id), peer: Peer::new(id.0 as u32), - app_state: app_state.clone(), + app_state, connection_pool: Default::default(), handlers: Default::default(), teardown: watch::channel(false).0, @@ -400,6 +400,8 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(multi_lsp_query) + .add_request_handler(lsp_query) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -910,7 +912,9 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, + // todo(lsp) remove after Zed Stable hits v0.204.x multi_lsp_query_request=field::Empty, + lsp_query_request=field::Empty, release_channel=field::Empty, { TOTAL_DURATION_MS }=field::Empty, { PROCESSING_DURATION_MS }=field::Empty, @@ -1386,9 +1390,7 @@ async fn create_room( let live_kit = live_kit?; let user_id = session.user_id().to_string(); - let token = live_kit - .room_token(&livekit_room, &user_id.to_string()) - .trace_err()?; + let token = live_kit.room_token(&livekit_room, &user_id).trace_err()?; Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), @@ -2015,9 +2017,9 @@ async fn join_project( .unzip(); response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, - worktrees: worktrees.clone(), + worktrees, replica_id: replica_id.0 as u32, - collaborators: collaborators.clone(), + collaborators, language_servers, language_server_capabilities, role: project.role.into(), @@ -2358,6 +2360,7 @@ where Ok(()) } +// todo(lsp) remove after Zed Stable hits v0.204.x async fn multi_lsp_query( request: MultiLspQuery, response: Response, @@ -2368,6 +2371,21 @@ async fn multi_lsp_query( forward_mutating_project_request(request, response, session).await } +async fn lsp_query( + request: proto::LspQuery, + response: Response, + session: MessageContext, +) -> Result<()> { + let (name, should_write) = request.query_name_and_write_permissions(); + tracing::Span::current().record("lsp_query_request", name); + tracing::info!("lsp_query message received"); + if should_write { + forward_mutating_project_request(request, response, session).await + } else { + forward_read_only_project_request(request, response, session).await + } +} + /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7b95fdd458..59d66f1821 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -15,13 +15,14 @@ use editor::{ }, }; use fs::Fs; -use futures::{StreamExt, lock::Mutex}; +use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ FakeLspAdapter, language_settings::{AllLanguageSettings, InlayHintSettings}, }; +use lsp::LSP_REQUEST_TIMEOUT; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, @@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }) } +#[gpui::test] +async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + + let command_name = "test_command"; + let capabilities = lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: None, + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec![command_name.to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }; + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); + + client_a + .fs() + .insert_tree( + path!("/dir"), + json!({ + "one.rs": "const ONE: usize = 1;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, window, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + (lsp_store, buffer) + }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let long_request_time = LSP_REQUEST_TIMEOUT / 2; + let (request_started_tx, mut request_started_rx) = mpsc::unbounded(); + let requests_started = Arc::new(AtomicUsize::new(0)); + let requests_completed = Arc::new(AtomicUsize::new(0)); + let _lens_requests = fake_language_server + .set_request_handler::({ + let request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + move |params, cx| { + let mut request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + async move { + assert_eq!( + params.text_document.uri.as_str(), + uri!("file:///dir/one.rs") + ); + requests_started.fetch_add(1, atomic::Ordering::Release); + request_started_tx.send(()).await.unwrap(); + cx.background_executor().timer(long_request_time).await; + let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1; + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)), + command: Some(lsp::Command { + title: format!("LSP Command {i}"), + command: command_name.to_string(), + arguments: None, + }), + data: None, + }])) + } + } + }); + + // Move cursor to a location, this should trigger the code lens call. + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([7..7]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 1, + "Selection change should have initiated the first request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 2, + "Selection change should have initiated the second request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started for the 2nd time") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([2..2]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "Selection change should have initiated the third request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + + _first_task.await.unwrap(); + _second_task.await.unwrap(); + cx_b.run_until_parked(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "No selection changes should trigger no more code lens requests" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 3, + "After enough time, all 3 LSP requests should have been served by the language server" + ); + let resulting_lens_actions = editor_b + .update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.code_lens_actions(&buffer_b, cx) + }) + }) + .await + .unwrap() + .unwrap(); + assert_eq!( + resulting_lens_actions.len(), + 1, + "Should have fetched one code lens action, but got: {resulting_lens_actions:?}" + ); + assert_eq!( + resulting_lens_actions.first().unwrap().lsp_action.title(), + "LSP Command 3", + "Only the final code lens action should be in the data" + ) +} + #[gpui::test(iterations = 10)] async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; @@ -2908,7 +3114,7 @@ async fn test_lsp_pull_diagnostics( { assert!( - diagnostics_pulls_result_ids.lock().await.len() > 0, + !diagnostics_pulls_result_ids.lock().await.is_empty(), "Initial diagnostics pulls should report None at least" ); assert_eq!( @@ -3593,7 +3799,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let abs_path = project_a.read_with(cx_a, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -3647,20 +3853,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3680,20 +3882,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3713,20 +3911,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3746,20 +3940,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints_a.len()); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d9fd8ffeb2..1e0c915bcb 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // the follow. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A cycles through some tabs. workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 930e635dd8..5c73253048 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3208,7 +3208,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3237,7 +3237,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3266,7 +3266,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3295,7 +3295,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3304,7 +3304,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3313,7 +3313,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -4850,6 +4850,7 @@ async fn test_definition( let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -4885,6 +4886,7 @@ async fn test_definition( let definitions_2 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); @@ -4922,6 +4924,7 @@ async fn test_definition( let type_definitions = project_b .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -5060,7 +5063,7 @@ async fn test_references( ]))) .unwrap(); - let references = references.await.unwrap(); + let references = references.await.unwrap().unwrap(); executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { // User is informed that a request is no longer pending. @@ -5104,7 +5107,7 @@ async fn test_references( lsp_response_tx .unbounded_send(Err(anyhow!("can't find references"))) .unwrap(); - assert_eq!(references.await.unwrap(), []); + assert_eq!(references.await.unwrap().unwrap(), []); // User is informed that the request is no longer pending. executor.run_until_parked(); @@ -5505,7 +5508,8 @@ async fn test_lsp_hover( // Request hover information as the guest. let mut hovers = project_b .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) - .await; + .await + .unwrap(); assert_eq!( hovers.len(), 2, @@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } - let definitions = definitions.await.unwrap(); + let definitions = definitions.await.unwrap().unwrap(); assert_eq!( definitions.len(), 1, diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index c283a9fcd1..6fcd6d75cd 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -266,7 +266,7 @@ impl RandomizedTest for RandomChannelBufferTest { "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", ); } else { - prev_text = Some((user_id, text.clone())); + prev_text = Some((user_id, text)); } // Assert that all clients and the server agree about who is present in the diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index cd4cf69f60..ac5c4c54ca 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -643,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest { ); let project = project.await?; - client.dev_server_projects_mut().push(project.clone()); + client.dev_server_projects_mut().push(project); } ClientOperation::CreateWorktreeEntry { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 07ea1efc9d..fd5e3eefc1 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -370,8 +370,8 @@ impl TestServer { let client = TestClient { app_state, username: name.to_string(), - channel_store: cx.read(ChannelStore::global).clone(), - notification_store: cx.read(NotificationStore::global).clone(), + channel_store: cx.read(ChannelStore::global), + notification_store: cx.read(NotificationStore::global), state: Default::default(), }; client.wait_for_current_user(cx).await; @@ -897,7 +897,7 @@ impl TestClient { let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); let entity = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut(); // it might be nice to try and cleanup these at the end of each test. (entity, cx) } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 9993c0841c..61b3e05e48 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -66,7 +66,7 @@ impl ChannelView { channel_id, link_position, pane.clone(), - workspace.clone(), + workspace, window, cx, ); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5ed3907f6c..8aaf6c0aa2 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1038,7 +1038,7 @@ impl Render for ChatPanel { .cloned(); el.when_some(reply_message, |el, reply_message| { - let user_being_replied_to = reply_message.sender.clone(); + let user_being_replied_to = reply_message.sender; el.child( h_flex() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0f785c1f90..d85a6610a5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1821,10 +1821,10 @@ impl CollabPanel { } fn select_channel_editor(&mut self) { - self.selection = self.entries.iter().position(|entry| match entry { - ListEntry::ChannelEditor { .. } => true, - _ => false, - }); + self.selection = self + .entries + .iter() + .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. })); } fn new_subchannel( @@ -2507,7 +2507,7 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { - let channel_link_copy = channel_link.clone(); + let channel_link_copy = channel_link; IconButton::new("channel-link", IconName::Copy) .icon_size(IconSize::Small) .size(ButtonSize::None) @@ -2691,7 +2691,7 @@ impl CollabPanel { h_flex() .w_full() .justify_between() - .child(Label::new(github_login.clone())) + .child(Label::new(github_login)) .child(h_flex().children(controls)), ) .start_slot(Avatar::new(user.avatar_uri.clone())) @@ -2905,6 +2905,8 @@ impl CollabPanel { h_flex().absolute().right(rems(0.)).h_full().child( h_flex() .h_full() + .bg(cx.theme().colors().background) + .rounded_l_sm() .gap_1() .px_1() .child( @@ -2920,8 +2922,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.join_channel_chat(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel chat")) - .visible_on_hover(""), + .tooltip(Tooltip::text("Open channel chat")), ) .child( IconButton::new("channel_notes", IconName::Reader) @@ -2936,9 +2937,9 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.open_channel_notes(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel notes")) - .visible_on_hover(""), - ), + .tooltip(Tooltip::text("Open channel notes")), + ) + .visible_on_hover(""), ), ) .tooltip({ @@ -3125,7 +3126,7 @@ impl Panel for CollabPanel { impl Focusable for CollabPanel { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a900d585f8..bf6fc3b224 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -289,7 +289,7 @@ impl NotificationPanel { .gap_1() .size_full() .overflow_hidden() - .child(Label::new(text.clone())) + .child(Label::new(text)) .child( h_flex() .child( diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b8800ff912..227d246f04 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -206,7 +206,7 @@ impl CommandPaletteDelegate { if parse_zed_link(&query, cx).is_some() { intercept_results = vec![CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), - string: query.clone(), + string: query, positions: vec![], }] } diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index 58bf1d8f0c..a840d520a6 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -42,7 +42,7 @@ impl RenderOnce for ComponentExample { div() .text_size(rems(0.875)) .text_color(cx.theme().colors().text_muted) - .child(description.clone()), + .child(description), ) }), ) diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index ccf7622d82..03cf047ac5 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -67,11 +67,7 @@ pub(crate) struct Client { pub(crate) struct ContextServerId(pub Arc); fn is_null_value(value: &T) -> bool { - if let Ok(Value::Null) = serde_json::to_value(value) { - true - } else { - false - } + matches!(serde_json::to_value(value), Ok(Value::Null)) } #[derive(Serialize, Deserialize)] diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 1b44cefbd2..4e5da2566e 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -112,7 +112,6 @@ impl McpServer { annotations: Some(tool.annotations()), }, handler: Box::new({ - let tool = tool.clone(); move |input_value, cx| { let input = match input_value { Some(input) => serde_json::from_value(input), diff --git a/crates/context_server/src/test.rs b/crates/context_server/src/test.rs index dedf589664..008542ab24 100644 --- a/crates/context_server/src/test.rs +++ b/crates/context_server/src/test.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use collections::HashMap; -use futures::{Stream, StreamExt as _, lock::Mutex}; +use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex}; use gpui::BackgroundExecutor; use std::{pin::Pin, sync::Arc}; @@ -14,9 +14,12 @@ pub fn create_fake_transport( executor: BackgroundExecutor, ) -> FakeTransport { let name = name.into(); - FakeTransport::new(executor).on_request::(move |_params| { - create_initialize_response(name.clone()) - }) + FakeTransport::new(executor).on_request::( + move |_params| { + let name = name.clone(); + async move { create_initialize_response(name.clone()) } + }, + ) } fn create_initialize_response(server_name: String) -> InitializeResponse { @@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse { } pub struct FakeTransport { - request_handlers: - HashMap<&'static str, Arc serde_json::Value + Send + Sync>>, + request_handlers: HashMap< + &'static str, + Arc BoxFuture<'static, serde_json::Value>>, + >, tx: futures::channel::mpsc::UnboundedSender, rx: Arc>>, executor: BackgroundExecutor, @@ -50,18 +55,25 @@ impl FakeTransport { } } - pub fn on_request( + pub fn on_request( mut self, - handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static, - ) -> Self { + handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut, + ) -> Self + where + T: crate::types::Request, + Fut: 'static + Send + Future, + { self.request_handlers.insert( T::METHOD, Arc::new(move |value| { - let params = value.get("params").expect("Missing parameters").clone(); + let params = value + .get("params") + .cloned() + .unwrap_or(serde_json::Value::Null); let params: T::Params = serde_json::from_value(params).expect("Invalid parameters received"); let response = handler(params); - serde_json::to_value(response).unwrap() + async move { serde_json::to_value(response.await).unwrap() }.boxed() }), ); self @@ -77,7 +89,7 @@ impl Transport for FakeTransport { if let Some(method) = msg.get("method") { let method = method.as_str().expect("Invalid method received"); if let Some(handler) = self.request_handlers.get(method) { - let payload = handler(msg); + let payload = handler(msg).await; let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 1916853a69..b7d8423fd7 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -81,10 +81,7 @@ pub fn init( }; copilot_chat::init(fs.clone(), http.clone(), configuration, cx); - let copilot = cx.new({ - let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(new_server_id, fs, node_runtime, cx) - }); + let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)); Copilot::set_global(copilot.clone(), cx); cx.observe(&copilot, |copilot, cx| { copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx)); @@ -129,7 +126,7 @@ impl CopilotServer { fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> { let server = self.as_running()?; anyhow::ensure!( - matches!(server.sign_in_status, SignInStatus::Authorized { .. }), + matches!(server.sign_in_status, SignInStatus::Authorized), "must sign in before using copilot" ); Ok(server) @@ -581,12 +578,12 @@ impl Copilot { pub(crate) fn sign_in(&mut self, cx: &mut Context) -> Task> { if let CopilotServer::Running(server) = &mut self.server { let task = match &server.sign_in_status { - SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(), + SignInStatus::Authorized => Task::ready(Ok(())).shared(), SignInStatus::SigningIn { task, .. } => { cx.notify(); task.clone() } - SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => { + SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => { let lsp = server.lsp.clone(); let task = cx .spawn(async move |this, cx| { @@ -730,7 +727,7 @@ impl Copilot { .. }) = &mut self.server { - if !matches!(status, SignInStatus::Authorized { .. }) { + if !matches!(status, SignInStatus::Authorized) { return; } @@ -1012,8 +1009,8 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => { match sign_in_status { - SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::Authorized => Status::Authorized, + SignInStatus::Unauthorized => Status::Unauthorized, SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { prompt: prompt.clone(), }, diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 2fd6df27b9..52d75175e5 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -301,6 +301,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -533,6 +534,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -1083,7 +1085,7 @@ mod tests { let replace_range_marker: TextRangeMarker = ('<', '>').into(); let (_, mut marked_ranges) = marked_text_ranges_by( marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], + vec![complete_from_marker, replace_range_marker.clone()], ); let replace_range = diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index f12913d1cb..370f0bb5f6 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true license = "GPL-3.0-or-later" [dependencies] +bincode.workspace = true crash-handler.workspace = true log.workspace = true minidumper.workspace = true @@ -14,6 +15,7 @@ release_channel.workspace = true smol.workspace = true serde.workspace = true serde_json.workspace = true +system_specs.workspace = true workspace-hack.workspace = true [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 4e4b69f639..f7bc96bff9 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -4,6 +4,8 @@ use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "macos")] +use std::sync::atomic::AtomicU32; use std::{ env, fs::{self, File}, @@ -26,6 +28,9 @@ pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg(target_os = "macos")] +static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0); + pub async fn init(crash_init: InitCrashHandler) { if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; @@ -110,9 +115,10 @@ unsafe fn suspend_all_other_threads() { mach2::task::task_threads(task, &raw mut threads, &raw mut count); } let current = unsafe { mach2::mach_init::mach_thread_self() }; + let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst); for i in 0..count { let t = unsafe { *threads.add(i as usize) }; - if t != current { + if t != current && t != panic_thread { unsafe { mach2::thread_act::thread_suspend(t) }; } } @@ -121,6 +127,7 @@ unsafe fn suspend_all_other_threads() { pub struct CrashServer { initialization_params: OnceLock, panic_info: OnceLock, + active_gpu: OnceLock, has_connection: Arc, } @@ -129,6 +136,8 @@ pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option, pub minidump_error: Option, + pub gpus: Vec, + pub active_gpu: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -137,7 +146,6 @@ pub struct InitCrashHandler { pub zed_version: String, pub release_channel: String, pub commit_sha: String, - // pub gpu: String, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -172,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer { Err(e) => Some(format!("{e:?}")), }; + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + let gpus = vec![]; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let gpus = match system_specs::read_gpu_info_from_sys_class_drm() { + Ok(gpus) => gpus, + Err(err) => { + log::warn!("Failed to collect GPU information for crash report: {err}"); + vec![] + } + }; + let crash_info = CrashInfo { init: self .initialization_params @@ -180,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer { .clone(), panic: self.panic_info.get().cloned(), minidump_error, + active_gpu: self.active_gpu.get().cloned(), + gpus, }; let crash_data_path = paths::logs_dir() @@ -205,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer { serde_json::from_slice::(&buffer).expect("invalid panic data"); self.panic_info.set(panic_data).expect("already panicked"); } + 3 => { + let gpu_specs: system_specs::GpuSpecs = + bincode::deserialize(&buffer).expect("gpu specs"); + self.active_gpu + .set(gpu_specs) + .expect("already set active gpu"); + } _ => { panic!("invalid message kind"); } @@ -238,6 +267,13 @@ pub fn handle_panic(message: String, span: Option<&Location>) { ) .ok(); log::error!("triggering a crash to generate a minidump..."); + + #[cfg(target_os = "macos")] + PANIC_THREAD_ID.store( + unsafe { mach2::mach_init::mach_thread_self() }, + Ordering::SeqCst, + ); + #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); #[cfg(not(target_os = "linux"))] @@ -274,6 +310,7 @@ pub fn crash_server(socket: &Path) { initialization_params: OnceLock::new(), panic_info: OnceLock::new(), has_connection, + active_gpu: OnceLock::new(), }), &shutdown, Some(CRASH_HANDLER_PING_TIMEOUT), diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7b791450ec..2590bf5c8b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -23,7 +23,7 @@ impl SessionId { Self(client_id as u32) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 131272da6b..c4338c6d00 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -664,7 +664,7 @@ impl ToolbarItemView for DapLogToolbarItemView { if let Some(item) = active_pane_item && let Some(log_view) = item.downcast::() { - self.log_view = Some(log_view.clone()); + self.log_view = Some(log_view); return workspace::ToolbarItemLocation::PrimaryLeft; } self.log_view = None; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6c70a935e0..f81c1fff89 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -386,10 +386,10 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = curr_session.read(cx).label(); let quirks = curr_session.read(cx).quirks(); - let adapter = curr_session.read(cx).adapter().clone(); + let adapter = curr_session.read(cx).adapter(); let binary = curr_session.read(cx).binary().cloned().unwrap(); let task_context = curr_session.read(cx).task_context().clone(); @@ -447,9 +447,9 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = self.label_for_child_session(&parent_session, request, cx); - let adapter = parent_session.read(cx).adapter().clone(); + let adapter = parent_session.read(cx).adapter(); let quirks = parent_session.read(cx).quirks(); let Some(mut binary) = parent_session.read(cx).binary().cloned() else { log::error!("Attempted to start a child-session without a binary"); @@ -932,7 +932,6 @@ impl DebugPanel { .cloned(), |this, running_state| { this.children({ - let running_state = running_state.clone(); let threads = running_state.update(cx, |running_state, cx| { let session = running_state.session(); @@ -1645,7 +1644,6 @@ impl Render for DebugPanel { } }) .on_action({ - let this = this.clone(); move |_: &ToggleSessionPicker, window, cx| { this.update(cx, |this, cx| { this.toggle_session_picker(window, cx); diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 5f5dfd1a1e..581cc16ff4 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -272,7 +272,6 @@ pub fn init(cx: &mut App) { } }) .on_action({ - let active_item = active_item.clone(); move |_: &ToggleIgnoreBreakpoints, _, cx| { active_item .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) @@ -293,9 +292,8 @@ pub fn init(cx: &mut App) { let Some(debug_panel) = workspace.read(cx).panel::(cx) else { return; }; - let Some(active_session) = debug_panel - .clone() - .update(cx, |panel, _| panel.active_session()) + let Some(active_session) = + debug_panel.update(cx, |panel, _| panel.active_session()) else { return; }; diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index dca15eb052..c5399f6f69 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -272,10 +272,9 @@ impl DebugPanel { .child(session_entry.label_element(self_depth, cx)) .child( IconButton::new("close-debug-session", IconName::Close) - .visible_on_hover(id.clone()) + .visible_on_hover(id) .icon_size(IconSize::Small) .on_click({ - let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |panel, cx| { panel.close_session(session_entity_id, window, cx); diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index eb0ad92dcc..b30e3995ff 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -785,7 +785,7 @@ impl RenderOnce for AttachMode { v_flex() .w_full() .track_focus(&self.attach_picker.focus_handle(cx)) - .child(self.attach_picker.clone()) + .child(self.attach_picker) } } diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index f0d7fd6fdd..cff2ba8335 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -256,7 +256,7 @@ pub(crate) fn deserialize_pane_layout( Some(Member::Axis(PaneAxis::load( if should_invert { axis.invert() } else { axis }, members, - flexes.clone(), + flexes, ))) } SerializedPaneLayout::Pane(serialized_pane) => { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index e3682ac991..9991395f35 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -180,7 +180,7 @@ impl SubView { let weak_list = list.downgrade(); let focus_handle = list.focus_handle(cx); let this = Self::new( - focus_handle.clone(), + focus_handle, list.into(), DebuggerPaneItem::BreakpointList, cx, @@ -916,7 +916,10 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let is_local = project.read(cx).is_local(); + let ssh_info = project + .read(cx) + .ssh_client() + .and_then(|it| it.read(cx).ssh_info()); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1000,7 +1003,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(is_local, &task.resolved.shell); + let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); @@ -1113,9 +1116,8 @@ impl RunningState { }; let session = self.session.read(cx); - let cwd = Some(&request.cwd) - .filter(|cwd| cwd.len() > 0) - .map(PathBuf::from) + let cwd = (!request.cwd.is_empty()) + .then(|| PathBuf::from(&request.cwd)) .or_else(|| session.binary().unwrap().cwd.clone()); let mut envs: HashMap = @@ -1150,7 +1152,7 @@ impl RunningState { } else { None } - } else if args.len() > 0 { + } else if !args.is_empty() { Some(args.remove(0)) } else { None @@ -1167,9 +1169,9 @@ impl RunningState { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), - command: command.clone(), + command, args, - command_label: title.clone(), + command_label: title, cwd, env: envs, use_new_terminal: true, @@ -1756,7 +1758,7 @@ impl RunningState { this.activate_item(0, false, false, window, cx); }); - let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx); rightmost_pane.update(cx, |this, cx| { this.add_item( Box::new(SubView::new( diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 0ba5b90aa1..a9518314e3 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -241,7 +241,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.breakpoints.len() - 1 { @@ -265,7 +265,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(self.breakpoints.len() - 1), Some(ix) => { if ix == 0 { @@ -283,7 +283,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(0) } else { None @@ -296,7 +296,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(self.breakpoints.len() - 1) } else { None @@ -649,7 +649,6 @@ impl BreakpointList { selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source), ) .on_click({ - let focus_handle = focus_handle.clone(); move |_, window, cx| { focus_handle.focus(window); window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx) @@ -1103,7 +1102,6 @@ impl ExceptionBreakpoint { } }) .on_click({ - let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_exception_breakpoint(&id, cx); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 05d2231da4..a801cedd26 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -365,7 +365,7 @@ impl Console { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Watch Expression", WatchExpression.boxed_clone()) })) diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 6b376bb892..921ebd8b5f 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -57,7 +57,7 @@ impl LoadedSourceList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(source.path.clone(), |this, path| this.child(path)), + .when_some(source.path, |this, path| this.child(path)), ) .into_any() } diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index 9b77ec75be..65dfd5fe0a 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -422,7 +422,7 @@ impl MemoryView { let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx); cx.spawn(async move |this, cx| { if let Some(info) = data_breakpoint_info.await { - let Some(data_id) = info.data_id.clone() else { + let Some(data_id) = info.data_id else { return; }; _ = this.update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index c7a8ebbadd..4ea763c92c 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -155,7 +155,7 @@ impl ModuleList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path.clone(), |this, path| this.child(path)), + .when_some(module.path, |this, path| this.child(path)), ) .into_any() } @@ -189,7 +189,7 @@ impl ModuleList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -209,7 +209,7 @@ impl ModuleList { cx: &mut Context, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -228,7 +228,7 @@ impl ModuleList { _window: &mut Window, cx: &mut Context, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -237,7 +237,7 @@ impl ModuleList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 6310042143..b3c500b919 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -123,7 +123,7 @@ impl StackFrameList { self.stack_frames(cx) .unwrap_or_default() .into_iter() - .map(|stack_frame| stack_frame.dap.clone()) + .map(|stack_frame| stack_frame.dap) .collect() } @@ -221,7 +221,7 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { - entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); + entries.push(StackFrameEntry::Collapsed(collapsed_entries)); } self.entries = entries; @@ -415,7 +415,7 @@ impl StackFrameList { let source = stack_frame.source.clone(); let is_selected_frame = Some(ix) == self.selected_ix; - let path = source.clone().and_then(|s| s.path.or(s.name)); + let path = source.and_then(|s| s.path.or(s.name)); let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,)); let formatted_path = formatted_path.map(|path| { Label::new(path) @@ -585,7 +585,7 @@ impl StackFrameList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -605,7 +605,7 @@ impl StackFrameList { cx: &mut Context, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -624,7 +624,7 @@ impl StackFrameList { _window: &mut Window, cx: &mut Context, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -633,7 +633,7 @@ impl StackFrameList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index f312032450..494e6c7f86 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -288,7 +288,7 @@ impl VariableList { } self.session.update(cx, |session, cx| { - session.variables(scope.variables_reference, cx).len() > 0 + !session.variables(scope.variables_reference, cx).is_empty() }) }) .map(|scope| { @@ -310,7 +310,7 @@ impl VariableList { watcher.variables_reference, watcher.variables_reference, EntryPath::for_watcher(watcher.expression.clone()), - DapEntry::Watcher(watcher.clone()), + DapEntry::Watcher(watcher), ) }) .collect::>(), @@ -994,7 +994,7 @@ impl VariableList { DapEntry::Watcher { .. } => continue, DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()), DapEntry::Scope(scope) => { - if scopes.len() > 0 { + if !scopes.is_empty() { idx += 1; } @@ -1298,8 +1298,6 @@ impl VariableList { IconName::Close, ) .on_click({ - let weak = weak.clone(); - let path = path.clone(); move |_, window, cx| { weak.update(cx, |variable_list, cx| { variable_list.selection = Some(path.clone()); @@ -1467,7 +1465,6 @@ impl VariableList { })) }) .on_secondary_mouse_down(cx.listener({ - let path = path.clone(); let entry = variable.clone(); move |this, event: &MouseDownEvent, window, cx| { this.selection = Some(path.clone()); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 6180831ea9..ab6d5cb960 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1330,7 +1330,6 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action( let called_set_breakpoints = Arc::new(AtomicBool::new(false)); client.on_request::({ - let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { assert!( args.breakpoints.is_none_or(|bps| bps.is_empty()), @@ -1445,7 +1444,6 @@ async fn test_we_send_arguments_from_user_config( let launch_handler_called = Arc::new(AtomicBool::new(false)); start_debug_session_with(&workspace, cx, debug_definition.clone(), { - let debug_definition = debug_definition.clone(); let launch_handler_called = launch_handler_called.clone(); move |client| { @@ -1783,9 +1781,8 @@ async fn test_debug_adapters_shutdown_on_app_quit( let disconnect_request_received = Arc::new(AtomicBool::new(false)); let disconnect_clone = disconnect_request_received.clone(); - let disconnect_clone_for_handler = disconnect_clone.clone(); client.on_request::(move |_, _| { - disconnect_clone_for_handler.store(true, Ordering::SeqCst); + disconnect_clone.store(true, Ordering::SeqCst); Ok(()) }); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 5ac6af389d..bfc445cf67 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -106,9 +106,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( ); let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { - input_path - .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) - .to_owned() + input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) } else { input_path.to_string() }; diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 53b5792e10..fd678078e8 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,7 +18,6 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true -futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2e20118381..1c27e820a0 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,7 +13,6 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; -use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -24,7 +23,6 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, - lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } -struct CargoDiagnosticsFetchState { - fetch_task: Option>, - cancel_task: Option>, - diagnostic_sources: Arc>, -} - impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_diagnostics(false, window, cx); - }) - .detach(); - cx.observe_release(&cx.entity(), |editor, _, cx| { - editor.stop_cargo_diagnostics_fetch(cx); + this.update_all_excerpts(window, cx); }) .detach(); @@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor { editor, paths_to_update: Default::default(), update_excerpts_task: None, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState { - fetch_task: None, - cancel_task: None, - diagnostic_sources: Arc::new(Vec::new()), - }, diagnostic_summary_update: Task::ready(()), _subscription: project_event_subscription, }; - this.update_all_diagnostics(true, window, cx); + this.update_all_excerpts(window, cx); this } @@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - - if fetch_cargo_diagnostics { - if self.cargo_diagnostics_fetch.fetch_task.is_some() { - self.stop_cargo_diagnostics_fetch(cx); - } else { - self.update_all_diagnostics(false, window, cx); - } - } else if self.update_excerpts_task.is_some() { + if self.update_excerpts_task.is_some() { self.update_excerpts_task = None; } else { - self.update_all_diagnostics(false, window, cx); + self.update_all_excerpts(window, cx); } cx.notify(); } @@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor { } } - fn update_all_diagnostics( - &mut self, - first_launch: bool, - window: &mut Window, - cx: &mut Context, - ) { - let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); - if cargo_diagnostics_sources.is_empty() { - self.update_all_excerpts(window, cx); - } else if first_launch && !self.summary.is_empty() { - self.update_all_excerpts(window, cx); - } else { - self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); - } - } - - fn fetch_cargo_diagnostics( - &mut self, - diagnostics_sources: Arc>, - cx: &mut Context, - ) { - let project = self.project.clone(); - self.cargo_diagnostics_fetch.cancel_task = None; - self.cargo_diagnostics_fetch.fetch_task = None; - self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); - if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { - return; - } - - self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { - let mut fetch_tasks = Vec::new(); - for buffer_path in diagnostics_sources.iter().cloned() { - if cx - .update(|cx| { - fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); - }) - .is_err() - { - break; - } - } - - let _ = join_all(fetch_tasks).await; - editor - .update(cx, |editor, _| { - editor.cargo_diagnostics_fetch.fetch_task = None; - }) - .ok(); - })); - } - - fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { - self.cargo_diagnostics_fetch.fetch_task = None; - let mut cancel_gasks = Vec::new(); - for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) - .iter() - .cloned() - { - cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); - } - - self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { - let _ = join_all(cancel_gasks).await; - log::info!("Finished fetching cargo diagnostics"); - })); - } - /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { @@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor { }) }) } - - pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if !fetch_cargo_diagnostics { - return Vec::new(); - } - self.project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; - let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { - entry - .path - .extension() - .and_then(|extension| extension.to_str()) - == Some("rs") - })?; - self.project.read(cx).path_for_entry(rust_file_entry.id, cx) - }) - .collect() - } } impl Focusable for ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 5df1b13897..4a544f9ea7 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -862,7 +862,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S 21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| { diagnostics.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); - if snapshot.buffer_snapshot.len() > 0 { + if !snapshot.buffer_snapshot.is_empty() { let position = rng.gen_range(0..snapshot.buffer_snapshot.len()); let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left); log::info!( diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index e77b80115f..404db39164 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -15,26 +13,18 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; - let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { - editor.read(cx).cargo_diagnostics_sources(cx) - })); - let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { let diagnostics = editor.read(cx); include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); - is_updating = if fetch_cargo_diagnostics { - diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() - } else { - diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some() - }; + is_updating = diagnostics.update_excerpts_task.is_some() + || diagnostics + .project + .read(cx) + .language_servers_running_disk_based_diagnostics(cx) + .next() + .is_some(); } let tooltip = if include_warnings { @@ -64,7 +54,6 @@ impl Render for ToolbarControls { .on_click(cx.listener(move |toolbar_controls, _, _, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { - diagnostics.stop_cargo_diagnostics_fetch(cx); diagnostics.update_excerpts_task = None; cx.notify(); }); @@ -76,7 +65,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) + .disabled(!has_stale_excerpts) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -84,17 +73,8 @@ impl Render for ToolbarControls { .on_click(cx.listener({ move |toolbar_controls, _, window, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { - let cargo_diagnostics_sources = - Arc::clone(&cargo_diagnostics_sources); diagnostics.update(cx, move |diagnostics, cx| { - if fetch_cargo_diagnostics { - diagnostics.fetch_cargo_diagnostics( - cargo_diagnostics_sources, - cx, - ); - } else { - diagnostics.update_all_excerpts(window, cx); - } + diagnostics.update_all_excerpts(window, cx); }); } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 99e588ada9..c900eb692a 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -61,15 +61,13 @@ impl PreprocessorError { for alias in action.deprecated_aliases { if alias == &action_name { return PreprocessorError::DeprecatedActionUsed { - used: action_name.clone(), + used: action_name, should_be: action.name.to_string(), }; } } } - PreprocessorError::ActionNotFound { - action_name: action_name.to_string(), - } + PreprocessorError::ActionNotFound { action_name } } } @@ -101,6 +99,7 @@ fn handle_preprocessing() -> Result<()> { let mut errors = HashSet::::new(); handle_frontmatter(&mut book, &mut errors); + template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); @@ -149,6 +148,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) }); } +fn template_big_table_of_actions(book: &mut Book) { + for_each_chapter_mut(book, |chapter| { + let needle = "{#ACTIONS_TABLE#}"; + if let Some(start) = chapter.content.rfind(needle) { + chapter.content.replace_range( + start..start + needle.len(), + &generate_big_table_of_actions(), + ); + } + }); +} + fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); @@ -279,6 +290,7 @@ struct ActionDef { name: &'static str, human_name: String, deprecated_aliases: &'static [&'static str], + docs: Option<&'static str>, } fn dump_all_gpui_actions() -> Vec { @@ -287,6 +299,7 @@ fn dump_all_gpui_actions() -> Vec { name: action.name, human_name: command_palette::humanize_action_name(action.name), deprecated_aliases: action.deprecated_aliases, + docs: action.documentation, }) .collect::>(); @@ -420,3 +433,54 @@ fn title_regex() -> &'static Regex { static TITLE_REGEX: OnceLock = OnceLock::new(); TITLE_REGEX.get_or_init(|| Regex::new(r"\s*(.*?)\s*").unwrap()) } + +fn generate_big_table_of_actions() -> String { + let actions = &*ALL_ACTIONS; + let mut output = String::new(); + + let mut actions_sorted = actions.iter().collect::>(); + actions_sorted.sort_by_key(|a| a.name); + + // Start the definition list with custom styling for better spacing + output.push_str("
\n"); + + for action in actions_sorted.into_iter() { + // Add the humanized action name as the term with margin + output.push_str( + "
", + ); + output.push_str(&action.human_name); + output.push_str("
\n"); + + // Add the definition with keymap name and description + output.push_str("
\n"); + + // Add the description, escaping HTML if needed + if let Some(description) = action.docs { + output.push_str( + &description + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"), + ); + output.push_str("
\n"); + } + output.push_str("Keymap Name: "); + output.push_str(action.name); + output.push_str("
\n"); + if !action.deprecated_aliases.is_empty() { + output.push_str("Deprecated Aliases:"); + for alias in action.deprecated_aliases.iter() { + output.push_str(""); + output.push_str(alias); + output.push_str(", "); + } + } + output.push_str("\n
\n"); + } + + // Close the definition list + output.push_str("
\n"); + + output +} diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index c8502f75de..6b695af1ae 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -34,7 +34,7 @@ pub enum DataCollectionState { impl DataCollectionState { pub fn is_supported(&self) -> bool { - !matches!(self, DataCollectionState::Unsupported { .. }) + !matches!(self, DataCollectionState::Unsupported) } pub fn is_enabled(&self) -> bool { @@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); - fn needs_terms_acceptance(&self, _cx: &App) -> bool { - false - } fn cycle( &mut self, buffer: Entity, @@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle { fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); - fn needs_terms_acceptance(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool; fn refresh( &self, @@ -196,10 +192,6 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } - fn needs_terms_acceptance(&self, cx: &App) -> bool { - self.read(cx).needs_terms_acceptance(cx) - } - fn is_refreshing(&self, cx: &App) -> bool { self.read(cx).is_refreshing() } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 21c934fefa..0e3fe8cb1a 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -168,7 +168,7 @@ impl Render for EditPredictionButton { let account_status = agent.account_status.clone(); match account_status { AccountStatus::NeedsActivation { activate_url } => { - SupermavenButtonStatus::NeedsActivation(activate_url.clone()) + SupermavenButtonStatus::NeedsActivation(activate_url) } AccountStatus::Unknown => SupermavenButtonStatus::Initializing, AccountStatus::Ready => SupermavenButtonStatus::Ready, @@ -242,13 +242,9 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal(&self.user_store, cx) { + if zeta::should_show_upsell_modal() { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - if self.user_store.read(cx).has_accepted_terms_of_service() { - "Choose a Plan" - } else { - "Accept the Terms of Service" - } + "Choose a Plan" } else { "Sign In" }; diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4847bc2565..96809d6877 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -514,7 +514,7 @@ impl CompletionsMenu { // Expand the range to resolve more completions than are predicted to be visible, to reduce // jank on navigation. let entry_indices = util::expanded_and_wrapped_usize_range( - entry_range.clone(), + entry_range, RESOLVE_BEFORE_ITEMS, RESOLVE_AFTER_ITEMS, entries.len(), diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5d5c9500eb..b073fe7be7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -290,7 +290,10 @@ pub enum Block { ExcerptBoundary { excerpt: ExcerptInfo, height: u32, - starts_new_buffer: bool, + }, + BufferHeader { + excerpt: ExcerptInfo, + height: u32, }, } @@ -303,27 +306,37 @@ impl Block { .. } => BlockId::ExcerptBoundary(next_excerpt.id), Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id), + Block::BufferHeader { + excerpt: next_excerpt, + .. + } => BlockId::ExcerptBoundary(next_excerpt.id), } } pub fn has_height(&self) -> bool { match self { Block::Custom(block) => block.height.is_some(), - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => true, } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height.unwrap_or(0), - Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height, + Block::ExcerptBoundary { height, .. } + | Block::FoldedBuffer { height, .. } + | Block::BufferHeader { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => BlockStyle::Sticky, } } @@ -332,6 +345,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -340,6 +354,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -351,6 +366,7 @@ impl Block { ), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -359,6 +375,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)), Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -367,6 +384,7 @@ impl Block { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -374,9 +392,8 @@ impl Block { match self { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, - Block::ExcerptBoundary { - starts_new_buffer, .. - } => *starts_new_buffer, + Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => true, } } } @@ -393,14 +410,14 @@ impl Debug for Block { .field("first_excerpt", &first_excerpt) .field("height", height) .finish(), - Self::ExcerptBoundary { - starts_new_buffer, - excerpt, - height, - } => f + Self::ExcerptBoundary { excerpt, height } => f .debug_struct("ExcerptBoundary") .field("excerpt", excerpt) - .field("starts_new_buffer", starts_new_buffer) + .field("height", height) + .finish(), + Self::BufferHeader { excerpt, height } => f + .debug_struct("BufferHeader") + .field("excerpt", excerpt) .field("height", height) .finish(), } @@ -662,13 +679,11 @@ impl BlockMap { }), ); - if buffer.show_headers() { - blocks_in_edit.extend(self.header_and_footer_blocks( - buffer, - (start_bound, end_bound), - wrap_snapshot, - )); - } + blocks_in_edit.extend(self.header_and_footer_blocks( + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); BlockMap::sort_blocks(&mut blocks_in_edit); @@ -771,7 +786,7 @@ impl BlockMap { if self.buffers_with_disabled_headers.contains(&new_buffer_id) { continue; } - if self.folded_buffers.contains(&new_buffer_id) { + if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() { let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { @@ -804,20 +819,24 @@ impl BlockMap { } } - if new_buffer_id.is_some() { + let starts_new_buffer = new_buffer_id.is_some(); + let block = if starts_new_buffer && buffer.show_headers() { height += self.buffer_header_height; - } else { + Block::BufferHeader { + excerpt: excerpt_boundary.next, + height, + } + } else if excerpt_boundary.prev.is_some() { height += self.excerpt_header_height; - } - - return Some(( - BlockPlacement::Above(WrapRow(wrap_row)), Block::ExcerptBoundary { excerpt: excerpt_boundary.next, height, - starts_new_buffer: new_buffer_id.is_some(), - }, - )); + } + } else { + continue; + }; + + return Some((BlockPlacement::Above(WrapRow(wrap_row)), block)); } }) } @@ -842,13 +861,25 @@ impl BlockMap { ( Block::ExcerptBoundary { excerpt: excerpt_a, .. + } + | Block::BufferHeader { + excerpt: excerpt_a, .. }, Block::ExcerptBoundary { excerpt: excerpt_b, .. + } + | Block::BufferHeader { + excerpt: excerpt_b, .. }, ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)), - (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less, - (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater, + ( + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + Block::Custom(_), + ) => Ordering::Less, + ( + Block::Custom(_), + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + ) => Ordering::Greater, (Block::Custom(block_a), Block::Custom(block_b)) => block_a .priority .cmp(&block_b.priority) @@ -1377,7 +1408,9 @@ impl BlockSnapshot { while let Some(transform) = cursor.item() { match &transform.block { - Some(Block::ExcerptBoundary { excerpt, .. }) => { + Some( + Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. }, + ) => { return Some(StickyHeaderExcerpt { excerpt }); } Some(block) if block.is_buffer_header() => return None, @@ -2156,7 +2189,7 @@ mod tests { } let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); @@ -2275,7 +2308,7 @@ mod tests { new_heights.insert(block_ids[0], 3); block_map_writer.resize(new_heights); - let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let snapshot = block_map.read(wraps_snapshot, Default::default()); // Same height as before, should remain the same assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n"); } @@ -2360,16 +2393,14 @@ mod tests { buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = inlay_map.sync( - buffer_snapshot.clone(), - buffer_subscription.consume().into_inner(), - ); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner()); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits); assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -2454,7 +2485,7 @@ mod tests { // Removing the replace block shows all the hidden blocks again. let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.remove(HashSet::from_iter([replace_block_id])); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!( blocks_snapshot.text(), "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5" @@ -2793,7 +2824,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id_3], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2846,7 +2877,7 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2860,7 +2891,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2868,12 +2899,7 @@ mod tests { 1, blocks .iter() - .filter(|(_, block)| { - match block { - Block::FoldedBuffer { .. } => true, - _ => false, - } - }) + .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) }) .count(), "Should have one folded block, producing a header of the second buffer" ); @@ -3190,9 +3216,9 @@ mod tests { // so we special case row 0 to assume a leading '\n'. // // Linehood is the birthright of strings. - let mut input_text_lines = input_text.split('\n').enumerate().peekable(); + let input_text_lines = input_text.split('\n').enumerate().peekable(); let mut block_row = 0; - while let Some((wrap_row, input_line)) = input_text_lines.next() { + for (wrap_row, input_line) in input_text_lines { let wrap_row = wrap_row as u32; let multibuffer_row = wraps_snapshot .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) @@ -3532,7 +3558,7 @@ mod tests { ..buffer_snapshot.anchor_after(Point::new(1, 0))], false, ); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno"); } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 3dcd172c3c..42f46fb749 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1557,7 +1557,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1636,7 +1636,7 @@ mod tests { let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1712,7 +1712,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1720,7 +1720,7 @@ mod tests { (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1747,7 +1747,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| { @@ -1782,7 +1782,7 @@ mod tests { let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]); let mut snapshot_edits = Vec::new(); let mut next_inlay_id = 0; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index eb5d57d484..6f5df9bb8e 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -116,7 +116,7 @@ impl TabMap { state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { - let new_state = edit.clone(); + let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result @@ -611,7 +611,7 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -628,7 +628,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -675,7 +675,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -689,7 +689,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,7 +749,7 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); @@ -758,7 +758,7 @@ mod tests { let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3ebfd7745..448852430e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -253,7 +253,6 @@ pub type RenderDiffHunkControlsFn = Arc< enum ReportEditorEvent { Saved { auto_saved: bool }, EditorOpened, - ZetaTosClicked, Closed, } @@ -262,7 +261,6 @@ impl ReportEditorEvent { match self { Self::Saved { .. } => "Editor Saved", Self::EditorOpened => "Editor Opened", - Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", Self::Closed => "Editor Closed", } } @@ -782,10 +780,7 @@ impl MinimapVisibility { } fn disabled(&self) -> bool { - match *self { - Self::Disabled => true, - _ => false, - } + matches!(*self, Self::Disabled) } fn settings_visibility(&self) -> bool { @@ -1857,8 +1852,8 @@ impl Editor { blink_manager }); - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); + let soft_wrap_mode_override = + matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); if full_mode && let Some(project) = project.as_ref() { @@ -1903,6 +1898,60 @@ impl Editor { editor.update_lsp_data(false, Some(*buffer_id), window, cx); } } + + project::Event::EntryRenamed(transaction) => { + let Some(workspace) = editor.workspace() else { + return; + }; + let Some(active_editor) = workspace.read(cx).active_item_as::(cx) + else { + return; + }; + if active_editor.entity_id() == cx.entity_id() { + let edited_buffers_already_open = { + let other_editors: Vec> = workspace + .read(cx) + .panes() + .iter() + .flat_map(|pane| pane.read(cx).items_of_type::()) + .filter(|editor| editor.entity_id() != cx.entity_id()) + .collect(); + + transaction.0.keys().all(|buffer| { + other_editors.iter().any(|editor| { + let multi_buffer = editor.read(cx).buffer(); + multi_buffer.read(cx).is_singleton() + && multi_buffer.read(cx).as_singleton().map_or( + false, + |singleton| { + singleton.entity_id() == buffer.entity_id() + }, + ) + }) + }) + }; + + if !edited_buffers_already_open { + let workspace = workspace.downgrade(); + let transaction = transaction.clone(); + cx.defer_in(window, move |_, window, cx| { + cx.spawn_in(window, async move |editor, cx| { + Self::open_project_transaction( + &editor, + workspace, + transaction, + "Rename".to_string(), + cx, + ) + .await + .ok() + }) + .detach(); + }); + } + } + } + _ => {} }, )); @@ -1983,14 +2032,12 @@ impl Editor { .detach(); } - let show_indent_guides = if matches!( - mode, - EditorMode::SingleLine { .. } | EditorMode::Minimap { .. } - ) { - Some(false) - } else { - None - }; + let show_indent_guides = + if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) { + Some(false) + } else { + None + }; let breakpoint_store = match (&mode, project.as_ref()) { (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), @@ -2050,7 +2097,7 @@ impl Editor { vertical: full_mode, }, minimap_visibility: MinimapVisibility::for_mode(&mode, cx), - offset_content: !matches!(mode, EditorMode::SingleLine { .. }), + offset_content: !matches!(mode, EditorMode::SingleLine), show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, show_gutter: full_mode, show_line_numbers: (!full_mode).then_some(false), @@ -2404,7 +2451,7 @@ impl Editor { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", + EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Minimap { .. } => "minimap", EditorMode::Full { .. } => "full", @@ -4531,7 +4578,7 @@ impl Editor { let mut char_position = 0u32; let mut end_tag_offset = None; - 'outer: for chunk in snapshot.text_for_range(range.clone()) { + 'outer: for chunk in snapshot.text_for_range(range) { if let Some(byte_pos) = chunk.find(&**end_tag) { let chars_before_match = chunk[..byte_pos].chars().count() as u32; @@ -4881,11 +4928,7 @@ impl Editor { cx: &mut Context, ) -> bool { let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) - else { + let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else { return false; }; @@ -5531,6 +5574,11 @@ impl Editor { .as_ref() .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); + let omit_word_completions = match &query { + Some(query) => query.chars().count() < completion_settings.words_min_length, + None => completion_settings.words_min_length != 0, + }; + let (mut words, provider_responses) = match &provider { Some(provider) => { let provider_responses = provider.completions( @@ -5542,9 +5590,11 @@ impl Editor { cx, ); - let words = match completion_settings.words { - WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), - WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + let words = match (omit_word_completions, completion_settings.words) { + (true, _) | (_, WordsCompletionMode::Disabled) => { + Task::ready(BTreeMap::default()) + } + (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5556,16 +5606,20 @@ impl Editor { (words, provider_responses) } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, + None => { + let words = if omit_word_completions { + Task::ready(BTreeMap::default()) + } else { + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) }) - }), - Task::ready(Ok(Vec::new())), - ), + }; + (words, Task::ready(Ok(Vec::new()))) + } }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -5849,7 +5903,7 @@ impl Editor { multibuffer_anchor.start.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot) }; - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) { return None; } @@ -6272,7 +6326,7 @@ impl Editor { })) } CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); + let context = actions_menu.actions.context; workspace.update(cx, |workspace, cx| { dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); @@ -6291,7 +6345,7 @@ impl Editor { } pub async fn open_project_transaction( - this: &WeakEntity, + editor: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -6309,7 +6363,7 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = this.update(cx, |editor, cx| { + let excerpt = editor.update(cx, |editor, cx| { editor .buffer() .read(cx) @@ -6472,7 +6526,7 @@ impl Editor { fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx); let buffer = self.buffer.read(cx); if newest_selection.head().diff_base_anchor.is_some() { return None; @@ -6706,7 +6760,6 @@ impl Editor { return; } - let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); if buffer .text_anchor_for_position(cursor_position, cx) @@ -6719,8 +6772,8 @@ impl Editor { let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + let buffer_id = cursor_buffer.read(cx).remote_id(); + for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx) { let start = highlight .range @@ -6735,12 +6788,12 @@ impl Editor { } let range = Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: start, diff_base_anchor: None, }..Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: end, diff_base_anchor: None, @@ -6775,7 +6828,7 @@ impl Editor { &mut self, cx: &mut Context, ) -> Option<(String, Range)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { return None; } if !EditorSettings::get_global(cx).selection_highlight { @@ -8191,8 +8244,6 @@ impl Editor { .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 @@ -9127,45 +9178,6 @@ impl Editor { let provider = self.edit_prediction_provider.as_ref()?; let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); - if provider.provider.needs_terms_acceptance(cx) { - return Some( - h_flex() - .min_w(min_width) - .flex_1() - .px_2() - .py_1() - .gap_3() - .elevation_2(cx) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .id("accept-terms") - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(cx.listener(|this, _event, window, cx| { - cx.stop_propagation(); - this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })) - .child( - h_flex() - .flex_1() - .gap_2() - .child(Icon::new(provider_icon)) - .child(Label::new("Accept Terms of Service")) - .child(div().w_full()) - .child( - Icon::new(IconName::ArrowUpRight) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element(), - ) - .into_any(), - ); - } - let is_refreshing = provider.provider.is_refreshing(cx); fn pending_completion_container(icon: IconName) -> Div { @@ -9507,17 +9519,21 @@ impl Editor { selection: Range, cx: &mut Context, ) { - let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) { - (Some(a), Some(b)) if a == b => a, - _ => { - log::error!("expected anchor range to have matching buffer IDs"); - return; - } - }; - let multi_buffer = self.buffer().read(cx); - let Some(buffer) = multi_buffer.buffer(*buffer_id) else { + let Some((_, buffer, _)) = self + .buffer() + .read(cx) + .excerpt_containing(selection.start, cx) + else { return; }; + let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx) + else { + return; + }; + if buffer != end_buffer { + log::error!("expected anchor range to have matching buffer IDs"); + return; + } let id = post_inc(&mut self.next_completion_id); let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -9763,6 +9779,9 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -9856,6 +9875,9 @@ impl Editor { } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { @@ -10604,16 +10626,12 @@ impl Editor { snapshot: &EditorSnapshot, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { - let project = self.project.clone()?; - - let buffer_id = breakpoint_position.buffer_id.or_else(|| { - snapshot - .buffer_snapshot - .buffer_id_for_excerpt(breakpoint_position.excerpt_id) - })?; + let buffer = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx)?; let enclosing_excerpt = breakpoint_position.excerpt_id; - let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let buffer_snapshot = buffer.read(cx).snapshot(); let row = buffer_snapshot @@ -10786,21 +10804,11 @@ impl Editor { return; }; - let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { - if breakpoint_position == Anchor::min() { - self.buffer() - .read(cx) - .excerpt_buffer_ids() - .into_iter() - .next() - } else { - None - } - }) else { - return; - }; - - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + let Some(buffer) = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx) + else { return; }; @@ -11028,7 +11036,7 @@ impl Editor { let mut col = 0; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { reindented_line.push(' '); @@ -11084,7 +11092,7 @@ impl Editor { let mut first_non_indent_char = None; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { // Keep track of spaces. Append \t when we reach tab_size @@ -12606,7 +12614,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -12730,7 +12738,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13214,7 +13222,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13235,7 +13243,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13256,7 +13264,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13277,7 +13285,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13298,7 +13306,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13323,7 +13331,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13348,7 +13356,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13373,7 +13381,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13398,7 +13406,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13419,7 +13427,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13440,7 +13448,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13461,7 +13469,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13482,7 +13490,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13507,7 +13515,7 @@ impl Editor { } pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -14556,7 +14564,7 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); + && !matches!(this.mode, EditorMode::SingleLine); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); @@ -14840,7 +14848,7 @@ impl Editor { if parent == child { return None; } - let text = buffer.text_for_range(child.clone()).collect::(); + let text = buffer.text_for_range(child).collect::(); Some((selection.id, parent, text)) }) .collect::>(); @@ -15443,7 +15451,8 @@ impl Editor { return; }; - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); + let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else { return; }; self.change_selections(Default::default(), window, cx, |s| { @@ -15721,7 +15730,9 @@ impl Editor { }; cx.spawn_in(window, async move |editor, cx| { - let definitions = definitions.await?; + let Some(definitions) = definitions.await? else { + return Ok(Navigated::No); + }; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -15943,7 +15954,7 @@ impl Editor { if !split && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); + editor.go_to_singleton_buffer_range(range, window, cx); } else { window.defer(cx, move |window, cx| { let target_editor: Entity = @@ -16063,7 +16074,9 @@ impl Editor { } }); - let locations = references.await?; + let Some(locations) = references.await? else { + return anyhow::Ok(Navigated::No); + }; if locations.is_empty() { return anyhow::Ok(Navigated::No); } @@ -16201,14 +16214,14 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); + workspace.split_item(SplitDirection::Right, item, window, cx); } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { let (preview_item_id, preview_item_idx) = workspace.active_pane().read_with(cx, |pane, _| { (pane.preview_item_id(), pane.preview_item_idx()) }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx); if let Some(preview_item_id) = preview_item_id { workspace.active_pane().update(cx, |pane, cx| { @@ -16216,7 +16229,7 @@ impl Editor { }); } } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + workspace.add_item_to_active_pane(item, None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -19007,10 +19020,7 @@ impl Editor { let selection = text::ToPoint::to_point(&range.start, buffer).row ..text::ToPoint::to_point(&range.end, buffer).row; - Some(( - multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), - selection, - )) + Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)) }); let Some((buffer, selection)) = buffer_and_selection else { @@ -19252,7 +19262,7 @@ impl Editor { row_highlights.insert( ix, RowHighlight { - range: range.clone(), + range, index, color, options, @@ -20435,11 +20445,8 @@ impl Editor { .range_to_buffer_ranges_with_deleted_hunks(selection.range()) { if let Some(anchor) = anchor { - // selection is in a deleted hunk - let Some(buffer_id) = anchor.buffer_id else { - continue; - }; - let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx) + else { continue; }; let offset = text::ToOffset::to_offset( @@ -21679,7 +21686,7 @@ fn wrap_with_prefix( 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 = first_line_prefix.clone(); + let mut current_line = first_line_prefix; let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); @@ -21851,7 +21858,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>; + ) -> Option>>>; fn inline_values( &self, @@ -21890,7 +21897,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>; + ) -> Option>>>>; fn range_for_rename( &self, @@ -22003,7 +22010,13 @@ impl CodeActionProvider for Entity { Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .chain(code_actions.context("code action fetch")?) + .flatten() + .chain( + code_actions + .context("code action fetch")? + .into_iter() + .flatten(), + ) .collect()) }) }) @@ -22298,7 +22311,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -22319,7 +22332,7 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { Some(self.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), @@ -22875,7 +22888,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -22901,7 +22914,7 @@ impl Render for Editor { } let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, + EditorMode::SingleLine => cx.theme().system().transparent, EditorMode::AutoHeight { .. } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index dc5557b052..91022d94a8 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "buffer-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1f1239ba0a..2cfdb92593 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,7 +57,9 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, + invalid_buffer_view::InvalidBufferView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, + register_project_item, }; #[gpui::test] @@ -708,7 +710,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { _ = workspace.update(cx, |_v, window, cx| { cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); let handle = cx.entity(); editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); @@ -898,7 +900,7 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -989,7 +991,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1074,7 +1076,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1173,7 +1175,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1335,7 +1337,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); assert_eq!('🟥'.len_utf8(), 4); @@ -1452,7 +1454,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -2479,7 +2481,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2527,7 +2529,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2563,7 +2565,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2608,7 +2610,7 @@ fn test_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2644,7 +2646,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), @@ -3175,7 +3177,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([3..4, 11..12, 19..20]) }); @@ -5562,7 +5564,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { # ˇThis is a long comment using a pound # sign. "}, - python_language.clone(), + python_language, &mut cx, ); @@ -5669,7 +5671,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { also very long and should not merge with the numbered item.ˇ» "}, - markdown_language.clone(), + markdown_language, &mut cx, ); @@ -5700,7 +5702,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // This is the second long comment block // to be wrapped.ˇ» "}, - rust_language.clone(), + rust_language, &mut cx, ); @@ -5723,7 +5725,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { «\tThis is a very long indented line \tthat will be wrapped.ˇ» "}, - plaintext_language.clone(), + plaintext_language, &mut cx, ); @@ -8889,7 +8891,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { @@ -9633,7 +9635,7 @@ async fn test_snippets(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -9713,7 +9715,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -10782,7 +10784,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { kind: Some("code-action-2".into()), edit: Some(lsp::WorkspaceEdit::new( [( - uri.clone(), + uri, vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), "applied-code-action-2-edit\n".to_string(), @@ -12237,6 +12239,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -12295,6 +12298,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -12331,6 +12335,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -13072,6 +13077,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -13168,6 +13174,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13231,6 +13238,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13304,6 +13312,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13361,6 +13370,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + words_min_length: 3, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, + }); + }); + + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + cx.set_state(indoc! {"ˇ + wow + wowen + wowser + "}); + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met" + ); + } + }); + + cx.simulate_keystroke("o"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met still" + ); + } + }); + + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); + } else { + panic!("expected completion menu to be open after the word completions threshold is met"); + } + }); +} + fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, @@ -14366,7 +14425,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); }); @@ -14543,7 +14602,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { ); let excerpt_ranges = markers.into_iter().map(|marker| { let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange::new(context.clone()) + ExcerptRange::new(context) }); let buffer = cx.new(|cx| Buffer::local(initial_text, cx)); let multibuffer = cx.new(|cx| { @@ -14828,7 +14887,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -15750,8 +15809,7 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon cx.simulate_keystroke("\n"); cx.run_until_parked(); - let buffer_cloned = - cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone()); + let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap()); let mut request = cx.set_request_handler::(move |_, _, mut cx| { let buffer_cloned = buffer_cloned.clone(); @@ -19455,7 +19513,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19546,7 +19604,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19612,7 +19670,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 1); @@ -19635,7 +19693,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( }); executor.run_until_parked(); - cx.assert_state_with_diff(hunk_expanded.clone()); + cx.assert_state_with_diff(hunk_expanded); } #[gpui::test] @@ -21031,7 +21089,7 @@ fn assert_breakpoint( path: &Arc, expected: Vec<(u32, Breakpoint)>, ) { - if expected.len() == 0usize { + if expected.is_empty() { assert!(!breakpoints.contains_key(path), "{}", path.display()); } else { let mut breakpoint = breakpoints @@ -21066,13 +21124,7 @@ fn add_log_breakpoint_at_cursor( let (anchor, bp) = editor .breakpoints_at_cursors(window, cx) .first() - .and_then(|(anchor, bp)| { - if let Some(bp) = bp { - Some((*anchor, bp.clone())) - } else { - None - } - }) + .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) .unwrap_or_else(|| { let cursor_position: Point = editor.selections.newest(cx).head(); @@ -21150,7 +21202,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21168,7 +21220,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21193,7 +21244,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21215,7 +21265,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints.len()); @@ -21267,7 +21316,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21282,7 +21331,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21303,7 +21351,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint(&breakpoints, &abs_path, vec![]); @@ -21323,7 +21370,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21346,7 +21392,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21369,7 +21414,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21442,7 +21486,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21462,7 +21506,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21494,7 +21537,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let disable_breakpoint = { @@ -21530,7 +21572,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -22509,10 +22550,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let closing_range = buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); let mut linked_ranges = HashMap::default(); - linked_ranges.insert( - buffer_id, - vec![(opening_range.clone(), vec![closing_range.clone()])], - ); + linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); }); let mut completion_handle = @@ -22677,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24264,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }) }) .unwrap(); @@ -24312,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_non_utf_8_opens(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.update(|cx| { + register_project_item::(cx); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root1", json!({})).await; + fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) + .await; + + let project = Project::test(fs, ["/root1".as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let handle = workspace + .update_in(cx, |workspace, window, cx| { + let project_path = (worktree_id, "one.pdf"); + workspace.open_path(project_path, None, true, window, cx) + }) + .await + .unwrap(); + + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); +} + #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 09cc0db7b8..70cd413c01 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -74,6 +74,7 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, + path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -89,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, - notifications::NotifyTaskExt, + CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, + item::Item, notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -2749,7 +2750,10 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; break; } @@ -2766,7 +2770,10 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; } block_height += block.height(); @@ -3452,42 +3459,41 @@ impl EditorElement { .into_any_element() } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - .. - } => { + Block::ExcerptBoundary { .. } => { let color = cx.theme().colors().clone(); let mut result = v_flex().id(block_id).w_full(); + result = result.child( + h_flex().relative().child( + div() + .top(line_height / 2.) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ), + ); + + result.into_any() + } + + Block::BufferHeader { excerpt, height } => { + let mut result = v_flex().id(block_id).w_full(); + let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt); - if *starts_new_buffer { - if sticky_header_excerpt_id != Some(excerpt.id) { - let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + if sticky_header_excerpt_id != Some(excerpt.id) { + let selected = selected_buffer_ids.contains(&excerpt.buffer_id); - result = result.child(div().pr(editor_margins.right).child( - self.render_buffer_header( - excerpt, false, selected, false, jump_data, window, cx, - ), - )); - } else { - result = - result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); - } - } else { - result = result.child( - h_flex().relative().child( - div() - .top(line_height / 2.) - .absolute() - .w_full() - .h_px() - .bg(color.border_variant), + result = result.child(div().pr(editor_margins.right).child( + self.render_buffer_header( + excerpt, false, selected, false, jump_data, window, cx, ), - ); - }; + )); + } else { + result = + result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); + } result.into_any() } @@ -3597,171 +3603,187 @@ impl EditorElement { let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - let header = - div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, + let header = div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, + window, + cx, + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers + editor.update(cx, |editor, cx| { + editor.toggle_fold_all( + &ToggleFoldAll, window, cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers + ); + }); + } else { + // Regular click toggles single buffer + if is_folded { editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, - window, - cx, - ); + editor.unfold_buffer(buffer_id, cx); }); } else { - // Regular click toggles single buffer - if is_folded { - editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); - }); - } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); } - }), - ), - ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), + } + }), + ), ) - .children(indicator) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some(file_status, |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled - } else { - Color::Created - }) - .when(status.is_deleted(), |el| el.strikethrough()) - }), - ) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), + ) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .map(|path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + path_header + .when(ItemSettings::get_global(cx).file_icons, |el| { + let path = path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + let icon = + Icon::from_path(icon).color(Color::Muted); + el.child(icon) + }) + .child(Label::new(filename).single_line().when_some( + file_status, + |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| { + el.strikethrough() + }) }, )) - }), - ) - .when( - can_open_excerpts && is_selected && relative_path.is_some(), - |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }, - ) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ); + }) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted + }, + )) + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); let file = for_excerpt.buffer.file().cloned(); let editor = self.editor.clone(); @@ -3777,25 +3799,31 @@ impl EditorElement { && let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx) { + let worktree = worktree.read(cx); let relative_path = file.path(); - let entry_for_path = worktree.read(cx).entry_for_path(relative_path); - let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); - let has_relative_path = - worktree.read(cx).root_entry().is_some_and(Entry::is_dir); + let entry_for_path = worktree.entry_for_path(relative_path); + let abs_path = entry_for_path.map(|e| { + e.canonical_path.as_deref().map_or_else( + || worktree.abs_path().join(relative_path), + Path::to_path_buf, + ) + }); + let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); - let parent_abs_path = - abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = abs_path + .as_ref() + .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); let relative_path = has_relative_path .then_some(relative_path) .map(ToOwned::to_owned); let visible_in_project_panel = - relative_path.is_some() && worktree.read(cx).is_visible(); + relative_path.is_some() && worktree.is_visible(); let reveal_in_project_panel = entry_for_path .filter(|_| visible_in_project_panel) .map(|entry| entry.id); menu = menu - .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { + .when_some(abs_path, |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), @@ -5708,7 +5736,10 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { Some(start_row) } else { None @@ -7209,7 +7240,7 @@ fn render_blame_entry_popover( ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?.clone(); + let repository = blame.repository(cx)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -8105,7 +8136,7 @@ impl Element for EditorElement { // The max scroll position for the top of the window let max_scroll_top = if matches!( snapshot.mode, - EditorMode::SingleLine { .. } + EditorMode::SingleLine | EditorMode::AutoHeight { .. } | EditorMode::Full { sized_by_content: true, @@ -9009,7 +9040,7 @@ impl Element for EditorElement { .as_ref() .map(|layout| (layout.bounds, layout.entry.clone())), display_hunks: display_hunks.clone(), - diff_hunk_control_bounds: diff_hunk_control_bounds.clone(), + diff_hunk_control_bounds, }); self.editor.update(cx, |editor, _| { @@ -9894,7 +9925,7 @@ impl CursorLayout { .px_0p5() .line_height(text_size + px(2.)) .text_color(cursor_name.color) - .child(cursor_name.string.clone()) + .child(cursor_name.string) .into_any_element(); name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx); diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index e38197283d..aa4e616924 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,6 +1,7 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, Window}; +use gpui::{Context, HighlightStyle, Window}; use language::CursorShape; +use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_background_highlights::(cx); + editor.clear_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_background::( - &[ + editor.highlight_text::( + vec![ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - |theme| theme.colors().editor_document_highlight_bracket_background, + HighlightStyle { + background_color: Some( + cx.theme() + .colors() + .editor_document_highlight_bracket_background, + ), + ..Default::default() + }, cx, ) } @@ -104,7 +112,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -115,7 +123,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -126,7 +134,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -138,7 +146,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -150,8 +158,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test("Test argument") { + cx.assert_editor_text_highlights::(indoc! {r#" + pub fn test«("Test argument") { another_test(1, 2, 3); } "#}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 04e66a234c..94f49f601a 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -321,7 +321,10 @@ pub fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { + if let Some(buffer_id) = snapshot + .buffer_snapshot + .buffer_id_for_anchor(previous_valid_anchor) + { inlay_hint_cache.spawn_hint_resolve( buffer_id, excerpt_id, @@ -559,7 +562,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().map(|definition_result| { + task.await.ok().flatten().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6efc88737a..e65c6b1807 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -174,11 +174,9 @@ pub fn hover_at_inlay( let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -428,7 +426,7 @@ fn show_hover( }; let hovers_response = if let Some(hover_request) = hover_request { - hover_request.await + hover_request.await.unwrap_or_default() } else { Vec::new() }; @@ -448,11 +446,9 @@ fn show_hover( let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -499,11 +495,9 @@ fn show_hover( hover_highlights.push(range.clone()); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -619,7 +613,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -668,7 +662,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index a1de2b604b..23717eeb15 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -164,8 +164,8 @@ pub fn indent_guides_in_range( let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset); let mut fold_ranges = Vec::>::new(); - let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); - while let Some(fold) = folds.next() { + let folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); + for fold in folds { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); if let Some(last_range) = fold_ranges.last_mut() diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index cea0e32d7f..dbf5ac95b7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -475,10 +475,7 @@ impl InlayHintCache { let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = shown_anchor - .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) - else { + let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8957e0e99c..641e8a97ed 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; @@ -103,9 +104,9 @@ impl FollowableItem for Editor { multibuffer = MultiBuffer::new(project.read(cx).capability()); let mut sorted_excerpts = state.excerpts.clone(); sorted_excerpts.sort_by_key(|e| e.id); - let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); + let sorted_excerpts = sorted_excerpts.into_iter().peekable(); - while let Some(excerpt) = sorted_excerpts.next() { + for excerpt in sorted_excerpts { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { continue; }; @@ -293,7 +294,7 @@ impl FollowableItem for Editor { EditorEvent::ExcerptsRemoved { ids, .. } => { update .deleted_excerpts - .extend(ids.iter().map(ExcerptId::to_proto)); + .extend(ids.iter().copied().map(ExcerptId::to_proto)); true } EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => { @@ -1401,6 +1402,16 @@ impl ProjectItem for Editor { editor } + + fn for_broken_project_item( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + } } fn clip_ranges<'a>( diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index a3fc41228f..e6c518beae 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -181,7 +181,7 @@ pub(crate) fn generate_auto_close_edits( */ { let tag_node_name_equals = |node: &Node, name: &str| { - let is_empty = name.len() == 0; + let is_empty = name.is_empty(); if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) { let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); @@ -207,7 +207,7 @@ pub(crate) fn generate_auto_close_edits( cur = descendant; } - assert!(ancestors.len() > 0); + assert!(!ancestors.is_empty()); let mut tree_root_node = open_tag; @@ -507,7 +507,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors().clone()) + .read_with(cx, |this, _| this.selections.disjoint_anchors()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index aaf9032b04..4f1313797f 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges( // Throw away selections spanning multiple buffers. continue; } - if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) { + if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) { applicable_selections.push(( buffer, start_position.text_anchor, diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 5cf22de537..3bc334c54c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -190,14 +190,16 @@ pub fn deploy_context_menu( .all::(cx) .into_iter() .any(|s| !s.is_empty()); - let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| { - project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some() - }); + let has_git_repo = buffer + .buffer_id_for_anchor(anchor) + .is_some_and(|buffer_id| { + project + .read(cx) + .git_store() + .read(cx) + .repository_and_path_for_buffer_id(buffer_id, cx) + .is_some() + }); let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); let run_to_cursor = window.is_action_available(&RunToCursor, cx); diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index c79feccb4b..2d4710a8d4 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index e3d83ab160..cf74ee0a9e 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { + if editor.read(cx).project().is_some_and(|project| { + project + .read(cx) + .language_server_statuses(cx) + .any(|(_, status)| status.name == RUST_ANALYZER_NAME) + }) { + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); + } + if editor .read(cx) .buffer() @@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(editor, window, go_to_parent_module); register_action(editor, window, expand_macro_recursively); register_action(editor, window, open_docs); - register_action(editor, window, cancel_flycheck_action); - register_action(editor, window, run_flycheck_action); - register_action(editor, window, clear_flycheck_action); } } @@ -309,7 +317,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -321,10 +329,7 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -337,7 +342,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -349,10 +354,7 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -365,7 +367,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -377,9 +379,6 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index c2bcdaf68f..828ab0594d 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -671,7 +671,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 72827b2fee..f8104665f9 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -16,7 +16,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index b2af4f8e4f..5992c9023c 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -67,10 +67,7 @@ impl ScrollAmount { } pub fn is_full_page(&self) -> bool { - match self { - ScrollAmount::Page(count) if count.abs() == 1.0 => true, - _ => false, - } + matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0) } pub fn direction(&self) -> ScrollDirection { diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index ab5e823f1c..54d8a50115 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -182,7 +182,9 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = signature_help.into_iter().next() else { + let Some(mut signature_help) = + signature_help.unwrap_or_default().into_iter().next() + else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d388e8f3b7..960fecf59a 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -230,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("§ -----"); } } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - } => { - if starts_new_buffer { - lines[row.0 as usize].push_str(&cx.update(|_, cx| { - format!( - "§ {}", - excerpt - .buffer - .file() - .unwrap() - .file_name(cx) - .to_string_lossy() - ) - })); - } else { - lines[row.0 as usize].push_str("§ -----") + Block::ExcerptBoundary { height, .. } => { + for row in row.0..row.0 + height { + lines[row as usize].push_str("§ -----"); } + } + Block::BufferHeader { excerpt, height } => { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); for row in row.0 + 1..row.0 + height { lines[row as usize].push_str("§ -----"); } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index c59786b1eb..3f78fa2f3e 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -300,6 +300,7 @@ impl EditorLspTestContext { self.to_lsp_range(ranges[0].clone()) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let start_point = range.start.to_point(&snapshot.buffer_snapshot); @@ -326,6 +327,7 @@ impl EditorLspTestContext { }) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let point = offset.to_point(&snapshot.buffer_snapshot); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index dbb519c40e..8c54c265ed 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -119,13 +119,7 @@ impl EditorTestContext { for excerpt in excerpts.into_iter() { let (text, ranges) = marked_text_ranges(excerpt, false); let buffer = cx.new(|cx| Buffer::local(text, cx)); - multibuffer.push_excerpts( - buffer, - ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())), - cx, - ); + multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx); } multibuffer }); @@ -426,7 +420,7 @@ impl EditorTestContext { if expected_text == "[FOLDED]\n" { assert!(is_folded, "excerpt {} should be folded", ix); let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id); - if expected_selections.len() > 0 { + if !expected_selections.is_empty() { assert!( is_selected, "excerpt {ix} should be selected. got {:?}", diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 1d2bece5cc..9e0504abca 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -103,7 +103,7 @@ fn main() { let languages: HashSet = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); - let app = Application::headless().with_http_client(http_client.clone()); + let app = Application::headless().with_http_client(http_client); let all_threads = examples::all(&examples_dir); app.run(move |cx| { @@ -416,11 +416,7 @@ pub fn init(cx: &mut App) -> Arc { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init( - LspAccess::Noop, - extension_host_proxy.clone(), - languages.clone(), - ); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); @@ -530,7 +526,7 @@ async fn judge_example( example_name = example.name.clone(), example_repetition = example.repetition, diff_evaluation = judge_output.diff.clone(), - thread_evaluation = judge_output.thread.clone(), + thread_evaluation = judge_output.thread, tool_metrics = run_output.tool_metrics, response_count = run_output.response_count, token_usage = run_output.token_usage, @@ -710,7 +706,7 @@ fn print_report( println!("Average thread score: {average_thread_score}%"); } - println!(""); + println!(); print_h2("CUMULATIVE TOOL METRICS"); println!("{}", cumulative_tool_metrics); diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 82e95728a1..457b62e98c 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -335,7 +335,7 @@ impl ExampleContext { for message in thread.messages().skip(message_count_before) { messages.push(Message { _role: message.role, - text: message.to_string(), + text: message.to_message_content(), tool_use: thread .tool_uses_for_message(message.id, cx) .into_iter() diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index bbbe54b43f..c6e4e0b6ec 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -90,11 +90,8 @@ impl ExampleInstance { worktrees_dir: &Path, repetition: usize, ) -> Self { - let name = thread.meta().name.to_string(); - let run_directory = run_dir - .join(&name) - .join(repetition.to_string()) - .to_path_buf(); + let name = thread.meta().name; + let run_directory = run_dir.join(&name).join(repetition.to_string()); let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); @@ -772,7 +769,7 @@ pub async fn query_lsp_diagnostics( } fn parse_assertion_result(response: &str) -> Result { - let analysis = get_tag("analysis", response)?.to_string(); + let analysis = get_tag("analysis", response)?; let passed = match get_tag("passed", response)?.to_lowercase().as_str() { "true" => true, "false" => false, @@ -916,9 +913,9 @@ impl RequestMarkdown { for tool in &request.tools { write!(&mut tools, "# {}\n\n", tool.name).unwrap(); write!(&mut tools, "{}\n\n", tool.description).unwrap(); - write!( + writeln!( &mut tools, - "{}\n", + "{}", MarkdownCodeBlock { tag: "json", text: &format!("{:#}", tool.input_schema) @@ -1192,7 +1189,7 @@ mod test { output.analysis, Some("The model did a good job but there were still compilations errors.".into()) ); - assert_eq!(output.passed, true); + assert!(output.passed); let response = r#" Text around ignored @@ -1212,6 +1209,6 @@ mod test { output.analysis, Some("Failed to compile:\n- Error 1\n- Error 2".into()) ); - assert_eq!(output.passed, false); + assert!(!output.passed); } } diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index aacc5d8795..72327179ee 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -232,10 +232,10 @@ pub trait Extension: Send + Sync { /// /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator: /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with - /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. + /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function - /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user - /// found the artifact path by themselves. + /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user + /// found the artifact path by themselves. /// /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case. diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 5a2093c1dd..5491967e08 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -145,7 +145,7 @@ mod tests { command: "*".to_string(), args: vec!["**".to_string()], })], - manifest.clone(), + manifest, ); assert!(granter.grant_exec("ls", &["-la"]).is_ok()); } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4c3ab8d242..fde0aeac94 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1175,16 +1175,16 @@ impl ExtensionStore { } } - for (server_id, _) in &extension.manifest.context_servers { + for server_id in extension.manifest.context_servers.keys() { self.proxy.unregister_context_server(server_id.clone(), cx); } - for (adapter, _) in &extension.manifest.debug_adapters { + for adapter in extension.manifest.debug_adapters.keys() { self.proxy.unregister_debug_adapter(adapter.clone()); } - for (locator, _) in &extension.manifest.debug_locators { + for locator in extension.manifest.debug_locators.keys() { self.proxy.unregister_debug_locator(locator.clone()); } - for (command_name, _) in &extension.manifest.slash_commands { + for command_name in extension.manifest.slash_commands.keys() { self.proxy.unregister_slash_command(command_name.clone()); } } @@ -1386,7 +1386,7 @@ impl ExtensionStore { ); } - for (id, _context_server_entry) in &manifest.context_servers { + for id in manifest.context_servers.keys() { this.proxy .register_context_server(extension.clone(), id.clone(), cx); } diff --git a/crates/extensions_ui/src/components/feature_upsell.rs b/crates/extensions_ui/src/components/feature_upsell.rs index 573b0b992d..0515dd46d3 100644 --- a/crates/extensions_ui/src/components/feature_upsell.rs +++ b/crates/extensions_ui/src/components/feature_upsell.rs @@ -61,7 +61,6 @@ impl RenderOnce for FeatureUpsell { .icon_size(IconSize::Small) .icon_position(IconPosition::End) .on_click({ - let docs_url = docs_url.clone(); move |_event, _window, cx| { telemetry::event!( "Documentation Viewed", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 10a29b4367..427e1a02aa 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -692,7 +692,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())) + .tooltip(Tooltip::text(repository_url)) })), ) } @@ -825,7 +825,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())), + .tooltip(Tooltip::text(repository_url)), ) .child( PopoverMenu::new(SharedString::from(format!( diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 3a2c1fd713..db872f7a15 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,13 +15,9 @@ path = "src/feedback.rs" test-support = [] [dependencies] -client.workspace = true gpui.workspace = true -human_bytes = "0.4.1" menu.workspace = true -release_channel.workspace = true -serde.workspace = true -sysinfo.workspace = true +system_specs.workspace = true ui.workspace = true urlencoding.workspace = true util.workspace = true diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 40c2707d34..3822dd7ba3 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,18 +1,14 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::SystemSpecs; +use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; use util::ResultExt; use workspace::Workspace; use zed_actions::feedback::FileBugReport; pub mod feedback_modal; -pub mod system_specs; - actions!( zed, [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, /// Opens email client to send feedback to Zed support. EmailZed, /// Opens the Zed repository on GitHub. diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3a08ec08e0..7512152324 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -267,10 +267,9 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { picker.delegate.include_ignored = match picker.delegate.include_ignored { - Some(true) => match FileFinderSettings::get_global(cx).include_ignored { - Some(_) => Some(false), - None => None, - }, + Some(true) => FileFinderSettings::get_global(cx) + .include_ignored + .map(|_| false), Some(false) => Some(true), None => Some(true), }; @@ -1402,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim().to_owned(); + let raw_query = raw_query.trim(); - let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { + let raw_query = raw_query.trim_end_matches(':').to_owned(); + let path = path_position.path.to_str(); + let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':'); + let file_query_end = if path_trimmed == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path_position.path.to_str().unwrap().len()) + Some(path.unwrap().len()) }; let query = FileSearchQuery { @@ -1750,7 +1752,7 @@ impl PickerDelegate for FileFinderDelegate { Some(ContextMenu::build(window, cx, { let focus_handle = focus_handle.clone(); move |menu, _, _| { - menu.context(focus_handle.clone()) + menu.context(focus_handle) .action( "Split Left", pane::SplitLeft.boxed_clone(), diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 8203d1b1fd..cd0f203d6a 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", + "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_matching_paths_with_colon(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + path!("/root"), + json!({ + "a": { + "foo:bar.rs": "", + "foo.rs": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; + + let (picker, _, cx) = build_find_picker(project, cx); + + // 'foo:' matches both files + cx.simulate_input("foo:"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); + + // 'foo:b' matches one of the files + cx.simulate_input("b"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + assert_match_at_position(picker, 0, "foo:bar.rs"); + }); + + cx.dispatch_action(editor::actions::Backspace); + + // 'foo:1' matches both files, specifying which row to jump to + cx.simulate_input("1"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); +} + #[gpui::test] async fn test_unicode_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index ffe3d42a27..4625872e46 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -653,7 +653,7 @@ impl PickerDelegate for OpenPathDelegate { if parent_path == &self.prompt_root { format!("{}{}", self.prompt_root, candidate.path.string) } else { - candidate.path.string.clone() + candidate.path.string }, match_positions, )), @@ -684,7 +684,7 @@ impl PickerDelegate for OpenPathDelegate { }; StyledText::new(label) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Conflict.color(cx)), @@ -694,7 +694,7 @@ impl PickerDelegate for OpenPathDelegate { } else { StyledText::new(format!("{label} (create)")) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Created.color(cx)), diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 5b093ac6a0..8a67eddcd7 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -345,7 +345,7 @@ impl GitRepository for FakeGitRepository { fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { self.with_state_async(true, move |state| { - state.branches.insert(name.to_owned()); + state.branches.insert(name); Ok(()) }) } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 11177512c3..75312c5c0c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1960,7 +1960,7 @@ impl FileHandle for FakeHandle { }; if state.try_entry(&target, false).is_some() { - return Ok(target.clone()); + return Ok(target); } anyhow::bail!("fake fd target not found") } @@ -2256,7 +2256,7 @@ impl Fs for FakeFs { async fn load(&self, path: &Path) -> Result { let content = self.load_internal(path).await?; - Ok(String::from_utf8(content.clone())?) + Ok(String::from_utf8(content)?) } async fn load_bytes(&self, path: &Path) -> Result> { @@ -2412,7 +2412,7 @@ impl Fs for FakeFs { tx, original_path: path.to_owned(), fs_state: self.state.clone(), - prefixes: Mutex::new(vec![path.to_owned()]), + prefixes: Mutex::new(vec![path]), }); ( Box::pin(futures::StreamExt::filter(rx, { diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index a5ce21294f..6ad03ba6df 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -159,7 +159,7 @@ impl GlobalWatcher { path: path.clone(), }; state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path.clone()).or_insert(0) += 1; + *state.path_registrations.entry(path).or_insert(0) += 1; Ok(id) } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 9c125d2c47..fd12dafa98 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -916,7 +916,7 @@ impl GitRepository for RealGitRepository { .context("no stdin for git cat-file subprocess")?; let mut stdin = BufWriter::new(stdin); for rev in &revs { - write!(&mut stdin, "{rev}\n")?; + writeln!(&mut stdin, "{rev}")?; } stdin.flush()?; drop(stdin); diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 92836042f2..71ca14c5b2 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -153,17 +153,11 @@ impl FileStatus { } pub fn is_conflicted(self) -> bool { - match self { - FileStatus::Unmerged { .. } => true, - _ => false, - } + matches!(self, FileStatus::Unmerged { .. }) } pub fn is_ignored(self) -> bool { - match self { - FileStatus::Ignored => true, - _ => false, - } + matches!(self, FileStatus::Ignored) } pub fn has_changes(&self) -> bool { @@ -176,40 +170,31 @@ impl FileStatus { pub fn is_modified(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Modified, _) | (_, StatusCode::Modified) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Modified, _) | (_, StatusCode::Modified) + ), _ => false, } } pub fn is_created(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Added, _) | (_, StatusCode::Added) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Added, _) | (_, StatusCode::Added) + ), FileStatus::Untracked => true, _ => false, } } pub fn is_deleted(self) -> bool { - match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true, - _ => false, - }, - _ => false, - } + matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted))) } pub fn is_untracked(self) -> bool { - match self { - FileStatus::Untracked => true, - _ => false, - } + matches!(self, FileStatus::Untracked) } pub fn summary(self) -> GitSummary { diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index f910de7bbe..2768e3dc68 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -172,7 +172,7 @@ impl BlameRenderer for GitBlameRenderer { .clone() .unwrap_or("".to_string()) .into(), - author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(), + author_email: blame.author_mail.unwrap_or("".to_string()).into(), message: details, }; @@ -186,7 +186,7 @@ impl BlameRenderer for GitBlameRenderer { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| commit_details.sha.clone()); - let full_sha = commit_details.sha.to_string().clone(); + let full_sha = commit_details.sha.to_string(); let absolute_timestamp = format_local_timestamp( commit_details.commit_time, OffsetDateTime::now_utc(), @@ -377,7 +377,7 @@ impl BlameRenderer for GitBlameRenderer { has_parent: true, }, repository.downgrade(), - workspace.clone(), + workspace, window, cx, ) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 6bb84db834..fb56cdcc5d 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -48,7 +48,7 @@ pub fn open( window: &mut Window, cx: &mut Context, ) { - let repository = workspace.project().read(cx).active_repository(cx).clone(); + let repository = workspace.project().read(cx).active_repository(cx); let style = BranchListStyle::Modal; workspace.toggle_modal(window, cx, |window, cx| { BranchList::new(repository, style, rems(34.), window, cx) @@ -144,7 +144,7 @@ impl BranchList { }) .detach_and_log_err(cx); - let delegate = BranchListDelegate::new(repository.clone(), style); + let delegate = BranchListDelegate::new(repository, style); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 4303f53275..cae4d28a83 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -35,7 +35,7 @@ impl ModalContainerProperties { // Calculate width based on character width let mut modal_width = 460.0; - let style = window.text_style().clone(); + let style = window.text_style(); let font_id = window.text_system().resolve_font(&style.font()); let font_size = style.font_size.to_pixels(window.rem_size()); @@ -179,7 +179,7 @@ impl CommitModal { let commit_editor = git_panel.update(cx, |git_panel, cx| { git_panel.set_modal_open(true, cx); - let buffer = git_panel.commit_message_buffer(cx).clone(); + let buffer = git_panel.commit_message_buffer(cx); let panel_editor = git_panel.commit_editor.clone(); let project = git_panel.project.clone(); @@ -285,7 +285,7 @@ impl CommitModal { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -391,15 +391,9 @@ impl CommitModal { }); let focus_handle = self.focus_handle(cx); - let close_kb_hint = - if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) { - Some( - KeybindingHint::new(close_kb, cx.theme().colors().editor_background) - .suffix("Cancel"), - ) - } else { - None - }; + let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| { + KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel") + }); h_flex() .group("commit_editor_footer") @@ -482,7 +476,7 @@ impl CommitModal { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", commit_label).into()), - Some(focus_handle.clone()), + Some(focus_handle), ) .into_any_element(), )), diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index 00ab911610..a470bc6925 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -181,7 +181,7 @@ impl Render for CommitTooltip { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| self.commit.sha.clone()); - let full_sha = self.commit.sha.to_string().clone(); + let full_sha = self.commit.sha.to_string(); let absolute_timestamp = format_local_timestamp( self.commit.commit_time, OffsetDateTime::now_utc(), diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 5c1b1325a5..ee1b82920d 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity, cx: &mu buffers: Default::default(), }); - let buffers = buffer.read(cx).all_buffers().clone(); + let buffers = buffer.read(cx).all_buffers(); for buffer in buffers { buffer_added(editor, buffer, cx); } @@ -129,7 +129,7 @@ fn buffer_added(editor: &mut Editor, buffer: Entity, cx: &mut Context>(); (goal_staged_state, entries) @@ -1304,7 +1304,6 @@ impl GitPanel { .read(cx) .as_singleton() .unwrap() - .clone() } fn toggle_staged_for_selected( @@ -2013,7 +2012,7 @@ impl GitPanel { let worktree = if worktrees.len() == 1 { Task::ready(Some(worktrees.first().unwrap().clone())) - } else if worktrees.len() == 0 { + } else if worktrees.is_empty() { let result = window.prompt( PromptLevel::Warning, "Unable to initialize a git repository", @@ -2595,22 +2594,22 @@ impl GitPanel { } } - if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 { + if conflict_entries.is_empty() && staged_count == 1 && pending_staged_count == 0 { match pending_status_for_single_staged { Some(TargetStatus::Staged) | None => { self.single_staged_entry = single_staged_entry; } _ => {} } - } else if conflict_entries.len() == 0 && pending_staged_count == 1 { + } else if conflict_entries.is_empty() && pending_staged_count == 1 { self.single_staged_entry = last_pending_staged; } - if conflict_entries.len() == 0 && changed_entries.len() == 1 { + if conflict_entries.is_empty() && changed_entries.len() == 1 { self.single_tracked_entry = changed_entries.first().cloned(); } - if conflict_entries.len() > 0 { + if !conflict_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Conflict, })); @@ -2618,7 +2617,7 @@ impl GitPanel { .extend(conflict_entries.into_iter().map(GitListEntry::Status)); } - if changed_entries.len() > 0 { + if !changed_entries.is_empty() { if !sort_by_path { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Tracked, @@ -2627,7 +2626,7 @@ impl GitPanel { self.entries .extend(changed_entries.into_iter().map(GitListEntry::Status)); } - if new_entries.len() > 0 { + if !new_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::New, })); @@ -2820,9 +2819,7 @@ impl GitPanel { let status_toast = StatusToast::new(message, cx, move |this, _cx| { use remote_output::SuccessStyle::*; match style { - Toast { .. } => { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - } + Toast => this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)), ToastWithLog { output } => this .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("View Log", move |window, cx| { @@ -3043,7 +3040,7 @@ impl GitPanel { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -3223,7 +3220,7 @@ impl GitPanel { let enable_coauthors = self.render_co_authors(cx); let editor_focus_handle = self.commit_editor.focus_handle(cx); - let expand_tooltip_focus_handle = editor_focus_handle.clone(); + let expand_tooltip_focus_handle = editor_focus_handle; let branch = active_repository.read(cx).branch.clone(); let head_commit = active_repository.read(cx).head_commit.clone(); @@ -3252,7 +3249,7 @@ impl GitPanel { display_name, branch, head_commit, - Some(git_panel.clone()), + Some(git_panel), )) .child( panel_editor_container(window, cx) @@ -3403,7 +3400,7 @@ impl GitPanel { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), - Some(commit_tooltip_focus_handle.clone()), + Some(commit_tooltip_focus_handle), cx, ) .into_any_element(), @@ -3469,7 +3466,7 @@ impl GitPanel { CommitView::open( commit.clone(), repo.clone(), - workspace.clone().clone(), + workspace.clone(), window, cx, ); @@ -3989,7 +3986,7 @@ impl GitPanel { } }) .child( - self.entry_label(display_name.clone(), label_color) + self.entry_label(display_name, label_color) .when(status.is_deleted(), |this| this.strikethrough()), ), ) @@ -4117,7 +4114,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option) -> Option) -> impl IntoElement { let project = self.project.read(cx); - let has_entries = self.entries.len() > 0; + let has_entries = !self.entries.is_empty(); let room = self .workspace .upgrade() @@ -4329,7 +4326,7 @@ impl GitPanelMessageTooltip { author_email: details.author_email.clone(), commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, message: Some(ParsedCommitMessage { - message: details.message.clone(), + message: details.message, ..Default::default() }), }; @@ -4462,7 +4459,7 @@ impl RenderOnce for PanelRepoFooter { }; let truncated_branch_name = if branch_actual_len <= branch_display_len { - branch_name.to_string() + branch_name } else { util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len) }; @@ -4475,7 +4472,7 @@ impl RenderOnce for PanelRepoFooter { let repo_selector = PopoverMenu::new("repository-switcher") .menu({ - let project = project.clone(); + let project = project; move |window, cx| { let project = project.clone()?; Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx))) @@ -4646,10 +4643,7 @@ impl Component for PanelRepoFooter { div() .w(example_width) .overflow_hidden() - .child(PanelRepoFooter::new_preview( - active_repository(1).clone(), - None, - )) + .child(PanelRepoFooter::new_preview(active_repository(1), None)) .into_any_element(), ), single_example( @@ -4658,7 +4652,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(2).clone(), + active_repository(2), Some(branch(unknown_upstream)), )) .into_any_element(), @@ -4669,7 +4663,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(3).clone(), + active_repository(3), Some(branch(no_remote_upstream)), )) .into_any_element(), @@ -4680,7 +4674,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(4).clone(), + active_repository(4), Some(branch(not_ahead_or_behind_upstream)), )) .into_any_element(), @@ -4691,7 +4685,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(5).clone(), + active_repository(5), Some(branch(behind_upstream)), )) .into_any_element(), @@ -4702,7 +4696,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(6).clone(), + active_repository(6), Some(branch(ahead_of_upstream)), )) .into_any_element(), @@ -4713,7 +4707,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(7).clone(), + active_repository(7), Some(branch(ahead_and_behind_upstream)), )) .into_any_element(), diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 3b4196b8ec..5369b8b404 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -245,12 +245,12 @@ fn render_remote_button( } (0, 0) => None, (ahead, 0) => Some(remote_button::render_push_button( - keybinding_target.clone(), + keybinding_target, id, ahead, )), (ahead, behind) => Some(remote_button::render_pull_button( - keybinding_target.clone(), + keybinding_target, id, ahead, behind, @@ -425,16 +425,9 @@ mod remote_button { let command = command.into(); if let Some(handle) = focus_handle { - Tooltip::with_meta_in( - label.clone(), - Some(action), - command.clone(), - &handle, - window, - cx, - ) + Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx) } else { - Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) + Tooltip::with_meta(label, Some(action), command, window, cx) } } @@ -457,7 +450,7 @@ mod remote_button { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Fetch", git::Fetch.boxed_clone()) .action("Fetch From", git::FetchFrom.boxed_clone()) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index cc1535b7c3..524dbf13d3 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -242,7 +242,7 @@ impl ProjectDiff { TRACKED_NAMESPACE }; - let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); + let path_key = PathKey::namespaced(namespace, entry.repo_path.0); self.move_to_path(path_key, window, cx) } @@ -448,10 +448,10 @@ impl ProjectDiff { let diff = diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) - .map(|diff_hunk| diff_hunk.buffer_range.clone()); + .map(|diff_hunk| diff_hunk.buffer_range); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) - .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) + .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); @@ -737,7 +737,7 @@ impl Render for ProjectDiff { } else { None }; - let keybinding_focus_handle = self.focus_handle(cx).clone(); + let keybinding_focus_handle = self.focus_handle(cx); el.child( v_flex() .gap_1() @@ -1070,8 +1070,7 @@ pub struct ProjectDiffEmptyState { impl RenderOnce for ProjectDiffEmptyState { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool { - match self.current_branch { - Some(Branch { + matches!(self.current_branch, Some(Branch { upstream: Some(Upstream { tracking: @@ -1081,9 +1080,7 @@ impl RenderOnce for ProjectDiffEmptyState { .. }), .. - }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true, - _ => false, - } + }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0)) }; let change_count = |current_branch: &Branch| -> (usize, usize) { diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index e38e3698d5..ebf32d1b99 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -48,7 +48,7 @@ impl TextDiffView { let selection_data = source_editor.update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); - let source_buffer = multibuffer.as_singleton()?.clone(); + let source_buffer = multibuffer.as_singleton()?; let selections = editor.selections.all::(cx); let buffer_snapshot = source_buffer.read(cx); let first_selection = selections.first()?; @@ -259,7 +259,7 @@ async fn update_diff_buffer( let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let base_text = base_buffer_snapshot.text().to_string(); + let base_text = base_buffer_snapshot.text(); let diff_snapshot = cx .update(|cx| { diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 23729be062..e60a3651aa 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -106,7 +106,7 @@ impl CursorPosition { cursor_position.selected_count.selections = editor.selections.count(); match editor.mode() { editor::EditorMode::AutoHeight { .. } - | editor::EditorMode::SingleLine { .. } + | editor::EditorMode::SingleLine | editor::EditorMode::Minimap { .. } => { cursor_position.position = None; cursor_position.context = None; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 908e61cac7..2afc72e989 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -157,7 +157,7 @@ impl GoToLine { self.prev_scroll_position.take(); cx.emit(DismissEvent) } - editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), + editor::EditorEvent::BufferEdited => self.highlight_current_line(cx), _ => {} } } @@ -712,7 +712,7 @@ mod tests { ) -> Entity { cx.dispatch_action(editor::actions::ToggleGoToLine); workspace.update(cx, |workspace, cx| { - workspace.active_modal::(cx).unwrap().clone() + workspace.active_modal::(cx).unwrap() }) } diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index ae635c94b8..37115feaa5 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -446,7 +446,7 @@ impl Element for TextElement { let (display_text, text_color) = if content.is_empty() { (input.placeholder.clone(), hsla(0., 0., 0., 0.2)) } else { - (content.clone(), style.color) + (content, style.color) }; let run = TextRun { @@ -474,7 +474,7 @@ impl Element for TextElement { }, TextRun { len: display_text.len() - marked_range.end, - ..run.clone() + ..run }, ] .into_iter() diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 1166bb2795..66e9cff0aa 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -155,7 +155,7 @@ impl RenderOnce for Specimen { .text_size(px(font_size * scale)) .line_height(relative(line_height)) .p(px(10.0)) - .child(self.string.clone()) + .child(self.string) } } diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index b179076cd5..0b824fec34 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -73,18 +73,18 @@ macro_rules! actions { /// - `name = "ActionName"` overrides the action's name. This must not contain `::`. /// /// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, -/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. +/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. /// /// - `no_register` skips registering the action. This is useful for implementing the `Action` trait -/// while not supporting invocation by name or JSON deserialization. +/// while not supporting invocation by name or JSON deserialization. /// /// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. -/// These action names should *not* correspond to any actions that are registered. These old names -/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will -/// accept these old names and provide warnings. +/// These action names should *not* correspond to any actions that are registered. These old names +/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will +/// accept these old names and provide warnings. /// /// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. -/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. +/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. /// /// # Manual Implementation /// diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index d9d21c0244..5eb4362904 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -465,7 +465,7 @@ impl VisualContext for AsyncWindowContext { V: Focusable, { self.window.update(self, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window); + view.read(cx).focus_handle(cx).focus(window); }) } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 6099ee5857..ea52b46d9f 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -231,14 +231,15 @@ impl AnyEntity { Self { entity_id: id, entity_type, - entity_map: entity_map.clone(), #[cfg(any(test, feature = "leak-detection"))] handle_id: entity_map + .clone() .upgrade() .unwrap() .write() .leak_detector .handle_created(id), + entity_map, } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 43adacf7dd..c65c045f6b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -134,7 +134,7 @@ impl TestAppContext { app: App::new_app(platform.clone(), asset_source, http_client), background_executor, foreground_executor, - dispatcher: dispatcher.clone(), + dispatcher, test_platform: platform, text_system, fn_name, @@ -192,6 +192,7 @@ impl TestAppContext { &self.foreground_executor } + #[expect(clippy::wrong_self_convention)] fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { let mut cx = self.app.borrow_mut(); cx.new(build_entity) @@ -244,7 +245,7 @@ impl TestAppContext { ) .unwrap(); drop(cx); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); cx } @@ -273,7 +274,7 @@ impl TestAppContext { .unwrap(); drop(cx); let view = window.root(self).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); // it might be nice to try and cleanup these at the end of each test. @@ -338,7 +339,7 @@ impl TestAppContext { /// Returns all windows open in the test. pub fn windows(&self) -> Vec { - self.app.borrow().windows().clone() + self.app.borrow().windows() } /// Run the given task on the main thread. @@ -618,7 +619,7 @@ impl Entity { } }), cx.subscribe(self, { - let mut tx = tx.clone(); + let mut tx = tx; move |_, _: &Evt, _| { tx.blocking_send(()).ok(); } @@ -882,7 +883,7 @@ impl VisualTestContext { /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). /// This method internally retains the VisualTestContext until the end of the test. - pub fn as_mut(self) -> &'static mut Self { + pub fn into_mut(self) -> &'static mut Self { let ptr = Box::into_raw(Box::new(self)); // safety: on_quit will be called after the test has finished. // the executor will ensure that all tasks related to the test have stopped. @@ -1025,7 +1026,7 @@ impl VisualContext for VisualTestContext { fn focus(&mut self, view: &Entity) -> Self::Result<()> { self.window .update(&mut self.cx, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window) + view.read(cx).focus_handle(cx).focus(window) }) .unwrap() } diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 639c84c101..cb7329c03f 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -905,9 +905,9 @@ mod tests { assert_eq!(background.solid, color); assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert_eq!(background.is_transparent(), false); + assert!(!background.is_transparent()); background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert_eq!(background.is_transparent(), true); + assert!(background.is_transparent()); } #[test] @@ -921,7 +921,7 @@ mod tests { assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); - assert_eq!(background.is_transparent(), false); - assert_eq!(background.opacity(0.0).is_transparent(), true); + assert!(!background.is_transparent()); + assert!(background.opacity(0.0).is_transparent()); } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index ae63819ca2..893860d7e1 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -475,7 +475,7 @@ impl Element for Img { .paint_image( new_bounds, corner_radii, - data.clone(), + data, layout_state.frame_index, self.style.grayscale, ) diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 2de3e23ff7..87cabc8cd9 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1046,7 +1046,7 @@ where size: self.size.clone() + size( amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom.clone(), + amount.top.clone() + amount.bottom, ), } } @@ -1159,10 +1159,10 @@ where /// Computes the space available within outer bounds. pub fn space_within(&self, outer: &Self) -> Edges { Edges { - top: self.top().clone() - outer.top().clone(), - right: outer.right().clone() - self.right().clone(), - bottom: outer.bottom().clone() - self.bottom().clone(), - left: self.left().clone() - outer.left().clone(), + top: self.top() - outer.top(), + right: outer.right() - self.right(), + bottom: outer.bottom() - self.bottom(), + left: self.left() - outer.left(), } } } @@ -1641,7 +1641,7 @@ impl Bounds { } /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(&self, factor: f32) -> Bounds { + pub fn to_device_pixels(self, factor: f32) -> Bounds { Bounds { origin: point( DevicePixels((self.origin.x.0 * factor).round() as i32), @@ -1712,7 +1712,7 @@ where top: self.top.clone() * rhs.top, right: self.right.clone() * rhs.right, bottom: self.bottom.clone() * rhs.bottom, - left: self.left.clone() * rhs.left, + left: self.left * rhs.left, } } } @@ -1957,7 +1957,7 @@ impl Edges { /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width /// ``` - pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { + pub fn to_pixels(self, parent_size: Size, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(parent_size.height, rem_size), right: self.right.to_pixels(parent_size.width, rem_size), @@ -2027,7 +2027,7 @@ impl Edges { /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Edges { + pub fn to_pixels(self, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(rem_size), right: self.right.to_pixels(rem_size), @@ -2272,7 +2272,7 @@ impl Corners { /// assert_eq!(corners_in_pixels.bottom_right, Pixels(30.0)); /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Corners { + pub fn to_pixels(self, rem_size: Pixels) -> Corners { Corners { top_left: self.top_left.to_pixels(rem_size), top_right: self.top_right.to_pixels(rem_size), @@ -2411,7 +2411,7 @@ where top_left: self.top_left.clone() * rhs.top_left, top_right: self.top_right.clone() * rhs.top_right, bottom_right: self.bottom_right.clone() * rhs.bottom_right, - bottom_left: self.bottom_left.clone() * rhs.bottom_left, + bottom_left: self.bottom_left * rhs.bottom_left, } } } @@ -2858,7 +2858,7 @@ impl DevicePixels { /// let total_bytes = pixels.to_bytes(bytes_per_pixel); /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes /// ``` - pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { + pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { self.0 as u32 * bytes_per_pixel as u32 } } @@ -3073,8 +3073,8 @@ pub struct Rems(pub f32); impl Rems { /// Convert this Rem value to pixels. - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { - *self * rem_size + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size } } @@ -3168,9 +3168,9 @@ impl AbsoluteLength { /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { match self { - AbsoluteLength::Pixels(pixels) => *pixels, + AbsoluteLength::Pixels(pixels) => pixels, AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), } } @@ -3184,10 +3184,10 @@ impl AbsoluteLength { /// # Returns /// /// Returns the `AbsoluteLength` as `Pixels`. - pub fn to_rems(&self, rem_size: Pixels) -> Rems { + pub fn to_rems(self, rem_size: Pixels) -> Rems { match self { AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), - AbsoluteLength::Rems(rems) => *rems, + AbsoluteLength::Rems(rems) => rems, } } } @@ -3315,12 +3315,12 @@ impl DefiniteLength { /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); /// ``` - pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { match self { DefiniteLength::Absolute(size) => size.to_pixels(rem_size), DefiniteLength::Fraction(fraction) => match base_size { - AbsoluteLength::Pixels(px) => px * *fraction, - AbsoluteLength::Rems(rems) => rems * rem_size * *fraction, + AbsoluteLength::Pixels(px) => px * fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * fraction, }, } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index f0ce04a915..0f5b98df39 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -172,6 +172,10 @@ pub trait AppContext { type Result; /// Create a new entity in the app context. + #[expect( + clippy::wrong_self_convention, + reason = "`App::new` is an ubiquitous function for creating entities" + )] fn new( &mut self, build_entity: impl FnOnce(&mut Context) -> T, @@ -348,7 +352,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug)] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct GpuSpecs { /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. pub is_software_emulated: bool, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 66f191ca5d..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -264,7 +264,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let (result, pending) = keymap.bindings_for_input( &[Keystroke::parse("ctrl-a").unwrap()], @@ -290,7 +290,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // binding is only enabled in a specific context assert!( @@ -344,7 +344,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space = || Keystroke::parse("space").unwrap(); let w = || Keystroke::parse("w").unwrap(); @@ -364,29 +364,29 @@ mod tests { // Ensure `space` results in pending input on the workspace, but not editor let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); assert!(space_workspace.0.is_empty()); - assert_eq!(space_workspace.1, true); + assert!(space_workspace.1); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, false); + assert!(!space_editor.1); // Ensure `space w` results in pending input on the workspace, but not editor let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); assert!(space_w_workspace.0.is_empty()); - assert_eq!(space_w_workspace.1, true); + assert!(space_w_workspace.1); let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); assert!(space_w_editor.0.is_empty()); - assert_eq!(space_w_editor.1, false); + assert!(!space_w_editor.1); // Ensure `space w w` results in the binding in the workspace, but not in the editor let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); assert!(!space_w_w_workspace.0.is_empty()); - assert_eq!(space_w_w_workspace.1, false); + assert!(!space_w_w_workspace.1); let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); assert!(space_w_w_editor.0.is_empty()); - assert_eq!(space_w_w_editor.1, false); + assert!(!space_w_w_editor.1); // Now test what happens if we have another binding defined AFTER the NoAction // that should result in pending @@ -396,11 +396,11 @@ mod tests { KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined BEFORE the NoAction // that should result in pending @@ -410,11 +410,11 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined at a higher context // that should result in pending @@ -424,11 +424,11 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); } #[test] @@ -439,7 +439,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -447,7 +447,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, true); + assert!(pending); let bindings = [ KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), @@ -455,7 +455,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -463,7 +463,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -474,7 +474,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -482,7 +482,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -494,7 +494,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -505,7 +505,7 @@ mod tests { ], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -516,7 +516,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -527,7 +527,7 @@ mod tests { ], ); assert_eq!(result.len(), 0); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -537,7 +537,7 @@ mod tests { KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -560,7 +560,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -579,7 +579,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -602,7 +602,7 @@ mod tests { KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -629,7 +629,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); assert_bindings(&keymap, &ActionBeta {}, &[]); diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 6d36cbb4e0..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -30,11 +30,8 @@ impl Clone for KeyBinding { impl KeyBinding { /// Construct a new keybinding from the given data. Panics on parse error. pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { - let context_predicate = if let Some(context) = context { - Some(KeyBindingContextPredicate::parse(context).unwrap().into()) - } else { - None - }; + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 976f99c26e..960bd1752f 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -668,11 +668,7 @@ mod tests { let contexts = vec![other_context.clone(), child_context.clone()]; assert!(!predicate.eval(&contexts)); - let contexts = vec![ - parent_context.clone(), - other_context.clone(), - child_context.clone(), - ]; + let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); @@ -681,7 +677,7 @@ mod tests { let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); assert!(!zany_predicate.eval(slice::from_ref(&child_context))); - assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context])); } #[test] @@ -718,7 +714,7 @@ mod tests { let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); assert!(!not_descendant.eval(slice::from_ref(&parent_context))); assert!(!not_descendant.eval(slice::from_ref(&child_context))); - assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context, child_context])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); assert!(double_not.eval(slice::from_ref(&editor_context))); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1df8a608f4..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -673,7 +673,7 @@ impl PlatformTextSystem for NoopTextSystem { } } let mut runs = Vec::default(); - if glyphs.len() > 0 { + if !glyphs.is_empty() { runs.push(ShapedRun { font_id: FontId(0), glyphs, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index ed824744a9..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -108,13 +108,13 @@ impl LinuxCommon { let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let common = LinuxCommon { background_executor, - foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), text_system, appearance: WindowAppearance::Light, auto_hide_scrollbars: false, @@ -667,7 +667,7 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; impl CursorStyle { #[cfg(any(feature = "wayland", feature = "x11"))] - pub(super) fn to_icon_names(&self) -> &'static [&'static str] { + pub(super) fn to_icon_names(self) -> &'static [&'static str] { // Based on cursor names from chromium: // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 match self { @@ -990,21 +990,18 @@ mod tests { #[test] fn test_is_within_click_distance() { let zero = Point::new(px(0.0), px(0.0)); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), - false - ); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); } } diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index cf73832b11..487bc9f38c 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -12,7 +12,7 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1:: use crate::CursorStyle; impl CursorStyle { - pub(super) fn to_shape(&self) -> Shape { + pub(super) fn to_shape(self) -> Shape { match self { CursorStyle::Arrow => Shape::Default, CursorStyle::IBeam => Shape::Text, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 4d31428094..189cfa1954 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -528,7 +528,7 @@ impl WaylandClient { client.common.appearance = appearance; - for (_, window) in &mut client.windows { + for window in client.windows.values_mut() { window.set_appearance(appearance); } } @@ -1280,7 +1280,6 @@ impl Dispatch for WaylandClientStatePtr { let Some(focused_window) = focused_window else { return; }; - let focused_window = focused_window.clone(); let keymap_state = state.keymap_state.as_ref().unwrap(); let keycode = Keycode::from(key + MIN_KEYCODE); diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index a21263ccfe..c7c9139dea 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -67,7 +67,7 @@ impl Cursor { { self.loaded_theme = Some(LoadedTheme { theme, - name: theme_name.map(|name| name.to_string()), + name: theme_name, scaled_size: self.scaled_size, }); } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index ce1468335d..7570c58c09 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1139,7 +1139,7 @@ fn update_window(mut state: RefMut) { } impl WindowDecorations { - fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { match self { WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, @@ -1148,7 +1148,7 @@ impl WindowDecorations { } impl ResizeEdge { - fn to_xdg(&self) -> xdg_toplevel::ResizeEdge { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { match self { ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 346ba8718b..9a43bd6470 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -456,7 +456,7 @@ impl X11Client { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { client.with_common(|common| common.appearance = appearance); - for (_, window) in &mut client.0.borrow_mut().windows { + for window in client.0.borrow_mut().windows.values_mut() { window.window.set_appearance(appearance); } } @@ -1329,7 +1329,7 @@ impl X11Client { state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.key_char = Some(text.clone()); + keystroke.key_char = Some(text); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, @@ -2108,7 +2108,7 @@ fn current_pointer_device_states( .classes .iter() .filter_map(|class| class.data.as_scroll()) - .map(|class| *class) + .copied() .rev() .collect::>(); let old_state = scroll_values_to_preserve.get(&info.deviceid); diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index c33d6fa462..6af943b317 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -95,7 +95,7 @@ fn query_render_extent( } impl ResizeEdge { - fn to_moveresize(&self) -> u32 { + fn to_moveresize(self) -> u32 { match self { ResizeEdge::TopLeft => 0, ResizeEdge::Top => 1, diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 49a5edceb2..9e5d6ec5ff 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -332,7 +332,7 @@ impl MetalRenderer { self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); if self.path_sample_count > 1 { - let mut msaa_descriptor = texture_descriptor.clone(); + let mut msaa_descriptor = texture_descriptor; msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private); msaa_descriptor.set_sample_count(self.path_sample_count as _); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index cd923a1859..4425d4fe24 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1090,7 +1090,7 @@ impl PlatformWindow for MacWindow { NSView::removeFromSuperview(blur_view); this.blurred_view = None; } - } else if this.blurred_view == None { + } else if this.blurred_view.is_none() { let content_view = this.native_window.contentView(); let frame = NSView::bounds(content_view); let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 2b4914baed..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -187,14 +187,14 @@ impl TestPlatform { .push_back(TestPrompt { msg: msg.to_string(), detail: detail.map(|s| s.to_string()), - answers: answers.clone(), + answers, tx, }); rx } pub(crate) fn set_active_window(&self, window: Option) { - let executor = self.foreground_executor().clone(); + let executor = self.foreground_executor(); let previous_window = self.active_window.borrow_mut().take(); self.active_window.borrow_mut().clone_from(&window); diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index e5b9c020d5..f554dea128 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,10 +9,8 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ - Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, - WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -56,12 +54,7 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAndOptionsAsync( - &handler, - WorkItemPriority::High, - WorkItemOptions::TimeSliced, - ) - .log_err(); + ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -72,12 +65,7 @@ impl WindowsDispatcher { Ok(()) }) }; - let delay = TimeSpan { - // A time period expressed in 100-nanosecond units. - // 10,000,000 ticks per second - Duration: (duration.as_nanos() / 100) as i64, - }; - ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); + ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); } } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 607163b577..4def6a11a5 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -956,7 +956,7 @@ impl WindowsWindowInner { click_count, first_mouse: false, }); - let result = func(input.clone()); + let result = func(input); let handled = !result.propagate || result.default_prevented; self.state.borrow_mut().callbacks.input = Some(func); diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index b13b9915f1..6202e05fb3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,5 +1,6 @@ use std::{ cell::RefCell, + ffi::OsStr, mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, @@ -460,13 +461,15 @@ impl Platform for WindowsPlatform { } fn open_url(&self, url: &str) { + if url.is_empty() { + return; + } let url_string = url.to_string(); self.background_executor() .spawn(async move { - if url_string.is_empty() { - return; - } - open_target(url_string.as_str()); + open_target(&url_string) + .with_context(|| format!("Opening url: {}", url_string)) + .log_err(); }) .detach(); } @@ -514,37 +517,29 @@ impl Platform for WindowsPlatform { } fn reveal_path(&self, path: &Path) { - let Ok(file_full_path) = path.canonicalize() else { - log::error!("unable to parse file path"); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(path) = file_full_path.to_str() else { - return; - }; - if path.is_empty() { - return; - } - open_target_in_explorer(path); + open_target_in_explorer(&path) + .with_context(|| format!("Revealing path {} in explorer", path.display())) + .log_err(); }) .detach(); } fn open_with_system(&self, path: &Path) { - let Ok(full_path) = path.canonicalize() else { - log::error!("unable to parse file full path: {}", path.display()); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(full_path_str) = full_path.to_str() else { - return; - }; - if full_path_str.is_empty() { - return; - }; - open_target(full_path_str); + open_target(&path) + .with_context(|| format!("Opening {} with system", path.display())) + .log_err(); }) .detach(); } @@ -735,39 +730,67 @@ pub(crate) struct WindowCreationInfo { pub(crate) disable_direct_composition: bool, } -fn open_target(target: &str) { - unsafe { - let ret = ShellExecuteW( +fn open_target(target: impl AsRef) -> Result<()> { + let target = target.as_ref(); + let ret = unsafe { + ShellExecuteW( None, windows::core::w!("open"), &HSTRING::from(target), None, None, SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!("Unable to open target: {}", std::io::Error::last_os_error()); - } + ) + }; + if ret.0 as isize <= 32 { + Err(anyhow::anyhow!( + "Unable to open target: {}", + std::io::Error::last_os_error() + )) + } else { + Ok(()) } } -fn open_target_in_explorer(target: &str) { +fn open_target_in_explorer(target: &Path) -> Result<()> { + let dir = target.parent().context("No parent folder found")?; + let desktop = unsafe { SHGetDesktopFolder()? }; + + let mut dir_item = std::ptr::null_mut(); unsafe { - let ret = ShellExecuteW( + desktop.ParseDisplayName( + HWND::default(), None, - windows::core::w!("open"), - windows::core::w!("explorer.exe"), - &HSTRING::from(format!("/select,{}", target).as_str()), + &HSTRING::from(dir), None, - SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!( - "Unable to open target in explorer: {}", - std::io::Error::last_os_error() - ); - } + &mut dir_item, + std::ptr::null_mut(), + )?; } + + let mut file_item = std::ptr::null_mut(); + unsafe { + desktop.ParseDisplayName( + HWND::default(), + None, + &HSTRING::from(target), + None, + &mut file_item, + std::ptr::null_mut(), + )?; + } + + let highlight = [file_item as *const _]; + unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { + if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). + open_target(dir).context("Opening target parent folder") + } else { + Err(anyhow::anyhow!("Can not open target path: {}", err)) + } + }) } fn file_open_dialog( diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 32a6da2391..99e5073371 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -592,10 +592,7 @@ impl PlatformWindow for WindowsWindow { ) -> Option> { let (done_tx, done_rx) = oneshot::channel(); let msg = msg.to_string(); - let detail_string = match detail { - Some(info) => Some(info.to_string()), - None => None, - }; + let detail_string = detail.map(|detail| detail.to_string()); let handle = self.0.hwnd; let answers = answers.to_vec(); self.0 diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index a34b7502f0..350184d350 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -108,7 +108,7 @@ impl From for Arc { fn from(val: SharedString) -> Self { match val.0 { ArcCow::Borrowed(borrowed) => Arc::from(borrowed), - ArcCow::Owned(owned) => owned.clone(), + ArcCow::Owned(owned) => owned, } } } diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 30d24e85e7..c4d2fda6e9 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -45,27 +45,18 @@ impl TabHandles { }) .unwrap_or_default(); - if let Some(next_handle) = self.handles.get(next_ix) { - Some(next_handle.clone()) - } else { - None - } + self.handles.get(next_ix).cloned() } pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option { let ix = self.current_index(focused_id).unwrap_or_default(); - let prev_ix; - if ix == 0 { - prev_ix = self.handles.len().saturating_sub(1); + let prev_ix = if ix == 0 { + self.handles.len().saturating_sub(1) } else { - prev_ix = ix.saturating_sub(1); - } + ix.saturating_sub(1) + }; - if let Some(prev_handle) = self.handles.get(prev_ix) { - Some(prev_handle.clone()) - } else { - None - } + self.handles.get(prev_ix).cloned() } } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 801d6b2567..1c67410043 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -164,7 +164,6 @@ impl TaffyLayoutEngine { // for (a, b) in self.get_edges(id)? { // println!("N{} --> N{}", u64::from(a), u64::from(b)); // } - // println!(""); // if !self.computed_layouts.insert(id) { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c4501daa59..0aa4cf804c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2453,7 +2453,7 @@ impl Window { /// time. pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { let (task, _) = cx.fetch_asset::(source); - task.clone().now_or_never() + task.now_or_never() } /// Obtain the current element offset. This method should only be called during the /// prepaint phase of element drawing. @@ -3044,7 +3044,7 @@ impl Window { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(¶ms.into(), &mut || { Ok(Some(( data.size(frame_index), Cow::Borrowed( @@ -3731,7 +3731,7 @@ impl Window { self.dispatch_keystroke_observers( event, Some(binding.action), - match_result.context_stack.clone(), + match_result.context_stack, cx, ); self.pending_input_changed(cx); @@ -4442,7 +4442,7 @@ impl Window { if let Some((_, inspector_id)) = self.hovered_inspector_hitbox(inspector, &self.rendered_frame) { - inspector.set_active_element_id(inspector_id.clone(), self); + inspector.set_active_element_id(inspector_id, self); } } }); @@ -4583,7 +4583,7 @@ impl WindowHandle { where C: AppContext, { - cx.read_window(self, |root_view, _cx| root_view.clone()) + cx.read_window(self, |root_view, _cx| root_view) } /// Check if this window is 'active'. diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 3a58af6705..0f1365be77 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -172,7 +172,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { /// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`. /// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass. /// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the -/// tests fail so that you can write out more detail about the failure. +/// tests fail so that you can write out more detail about the failure. /// /// You can combine `iterations = ...` with `seeds(...)`: /// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`. diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index 522c0a62c4..a0adcb7801 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -34,13 +34,6 @@ trait Transform: Clone { /// Adds one to the value fn add_one(self) -> Self; - - /// cfg attributes are respected - #[cfg(all())] - fn cfg_included(self) -> Self; - - #[cfg(any())] - fn cfg_omitted(self) -> Self; } #[derive(Debug, Clone, PartialEq)] @@ -70,10 +63,6 @@ impl Transform for Number { fn add_one(self) -> Self { Number(self.0 + 1) } - - fn cfg_included(self) -> Self { - Number(self.0) - } } #[test] @@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() { // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self let methods = methods::(); - assert_eq!(methods.len(), 6); + assert_eq!(methods.len(), 5); let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); assert!(method_names.contains(&"double")); assert!(method_names.contains(&"triple")); assert!(method_names.contains(&"increment")); assert!(method_names.contains(&"quadruple")); assert!(method_names.contains(&"add_one")); - assert!(method_names.contains(&"cfg_included")); // Invoke methods by name let num = Number(5); @@ -106,9 +94,7 @@ fn test_derive_inspector_reflection() { .invoke(num.clone()); assert_eq!(incremented, Number(6)); - let quadrupled = find_method::("quadruple") - .unwrap() - .invoke(num.clone()); + let quadrupled = find_method::("quadruple").unwrap().invoke(num); assert_eq!(quadrupled, Number(20)); // Try to invoke a non-existent method diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 46d5eafd5a..2d4abf4063 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,6 +13,7 @@ path = "src/gpui_tokio.rs" doctest = false [dependencies] +anyhow.workspace = true util.workspace = true gpui.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index fffe18a616..8384f2a88e 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,6 +52,28 @@ impl Tokio { }) } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> C::Result>> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.runtime.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } + pub fn handle(cx: &App) -> tokio::runtime::Handle { GlobalTokio::global(cx).runtime.handle().clone() } diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 473849f3cd..6b99a54a7d 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -40,7 +40,7 @@ impl AsyncBody { } pub fn from_bytes(bytes: Bytes) -> Self { - Self(Inner::Bytes(Cursor::new(bytes.clone()))) + Self(Inner::Bytes(Cursor::new(bytes))) } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 38f02c2206..4fc6039fd7 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -34,6 +34,7 @@ pub enum IconName { ArrowRightLeft, ArrowUp, ArrowUpRight, + Attach, AudioOff, AudioOn, Backspace, @@ -164,6 +165,7 @@ pub enum IconName { PageDown, PageUp, Pencil, + PencilUnavailable, Person, Pin, PlayOutlined, diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index 8e55a8a477..cefe888974 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,6 +24,7 @@ serde_json_lenient.workspace = true theme.workspace = true ui.workspace = true util.workspace = true +util_macros.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index 0c2b16b9f4..c3d687e57a 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,7 @@ use util::split_str_with_ranges; /// Path used for unsaved buffer that contains style json. To support the json language server, this /// matches the name used in the generated schemas. -const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; +const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json"); pub(crate) struct DivInspector { state: State, diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 81dc36093b..c09ab6f764 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -123,7 +123,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } let app_state = workspace.app_state().clone(); - let view_snapshot = workspace.weak_handle().clone(); + let view_snapshot = workspace.weak_handle(); window .spawn(cx, async move |cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index cc96022e63..4ddc2b3018 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -974,8 +974,6 @@ impl Buffer { TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } @@ -1020,9 +1018,6 @@ impl Buffer { let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); - let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } BufferSnapshot { @@ -1574,11 +1569,21 @@ impl Buffer { self.send_operation(op, true, cx); } - pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> { - let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else { - return None; - }; - Some(&self.diagnostics[idx].1) + pub fn buffer_diagnostics( + &self, + for_server: Option, + ) -> Vec<&DiagnosticEntry> { + match for_server { + Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) { + Ok(idx) => self.diagnostics[idx].1.iter().collect(), + Err(_) => Vec::new(), + }, + None => self + .diagnostics + .iter() + .flat_map(|(_, diagnostic_set)| diagnostic_set.iter()) + .collect(), + } } fn request_autoindent(&mut self, cx: &mut Context) { @@ -2206,7 +2211,7 @@ impl Buffer { self.remote_selections.insert( AGENT_REPLICA_ID, SelectionSet { - selections: selections.clone(), + selections, lamport_timestamp, line_mode, cursor_shape, @@ -3006,9 +3011,9 @@ impl BufferSnapshot { } let mut error_ranges = Vec::>::new(); - let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { - grammar.error_query.as_ref() - }); + let mut matches = self + .syntax + .matches(range, &self.text, |grammar| grammar.error_query.as_ref()); while let Some(mat) = matches.peek() { let node = mat.captures[0].node; let start = Point::from_ts_point(node.start_position()); @@ -4075,7 +4080,7 @@ impl BufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - for pair in self.enclosing_bracket_ranges(range.clone()) { + for pair in self.enclosing_bracket_ranges(range) { if let Some(range_filter) = range_filter && !range_filter(pair.open_range.clone(), pair.close_range.clone()) { @@ -4248,7 +4253,7 @@ impl BufferSnapshot { .map(|(range, name)| { ( name.to_string(), - self.text_for_range(range.clone()).collect::(), + self.text_for_range(range).collect::(), ) }) .collect(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 2e2df7e658..ce65afa628 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1744,7 +1744,7 @@ fn test_autoindent_block_mode(cx: &mut App) { buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1790,9 +1790,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) { "# .unindent(); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1843,7 +1843,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) { buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -2030,7 +2030,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); language_registry.add(html_language.clone()); - language_registry.add(javascript_language.clone()); + language_registry.add(javascript_language); cx.new(|cx| { let (text, ranges) = marked_text_ranges( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 87fc846a53..7ae77c9141 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -206,7 +206,7 @@ impl CachedLspAdapter { } pub fn name(&self) -> LanguageServerName { - self.adapter.name().clone() + self.adapter.name() } pub async fn get_language_server_command( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 589fc68e99..4f07240e44 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -49,7 +49,7 @@ impl LanguageName { pub fn from_proto(s: String) -> Self { Self(SharedString::from(s)) } - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { self.0.to_string() } pub fn lsp_id(&self) -> String { @@ -432,7 +432,7 @@ impl LanguageRegistry { let mut state = self.state.write(); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(adapter.clone()); state.all_lsp_adapters.insert(adapter.name(), adapter); @@ -454,7 +454,7 @@ impl LanguageRegistry { let cached_adapter = CachedLspAdapter::new(Arc::new(adapter)); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(cached_adapter.clone()); state @@ -1167,8 +1167,7 @@ impl LanguageRegistryState { soft_wrap: language.config.soft_wrap, auto_indent_on_paste: language.config.auto_indent_on_paste, ..Default::default() - } - .clone(), + }, ); self.languages.push(language); self.version += 1; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index fbb67a9818..0f82d3997f 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -5,7 +5,7 @@ use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, + property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; @@ -199,7 +199,7 @@ impl LanguageSettings { if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { rest.clone() } else { - vec![language_server.clone()] + vec![language_server] } }) .collect::>() @@ -350,6 +350,12 @@ pub struct CompletionSettings { /// Default: `fallback` #[serde(default = "default_words_completion_mode")] pub words: WordsCompletionMode, + /// How many characters has to be in the completions query to automatically show the words-based completions. + /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + /// + /// Default: 3 + #[serde(default = "default_3")] + pub words_min_length: usize, /// Whether to fetch LSP completions or not. /// /// Default: true @@ -359,7 +365,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default = "default_lsp_fetch_timeout_ms")] + #[serde(default)] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -405,8 +411,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_lsp_fetch_timeout_ms() -> u64 { - 0 +fn default_3() -> usize { + 3 } /// The settings for a particular language. @@ -1131,6 +1137,10 @@ impl AllLanguageSettings { } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { + let preferred_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); let tab_size = cfg.get::().ok().and_then(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { @@ -1158,6 +1168,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr *target = value; } } + merge(&mut settings.preferred_line_length, preferred_line_length); merge(&mut settings.tab_size, tab_size); merge(&mut settings.hard_tabs, hard_tabs); merge( @@ -1463,6 +1474,7 @@ impl settings::Settings for AllLanguageSettings { } else { d.completions = Some(CompletionSettings { words: mode, + words_min_length: 3, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::ReplaceSuffix, @@ -1793,7 +1805,7 @@ mod tests { assert!(!settings.enabled_for_file(&dot_env_file, &cx)); // Test tilde expansion - let home = shellexpand::tilde("~").into_owned().to_string(); + let home = shellexpand::tilde("~").into_owned(); let home_file = make_test_file(&[&home, "test.rs"]); let settings = build_settings(&["~/test.rs"]); assert!(!settings.enabled_for_file(&home_file, &cx)); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f10056af13..38aad007fe 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -832,7 +832,7 @@ impl SyntaxSnapshot { query: fn(&Grammar) -> Option<&Query>, ) -> SyntaxMapCaptures<'a> { SyntaxMapCaptures::new( - range.clone(), + range, text, [SyntaxLayer { language, diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index d576c95cd5..622731b781 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -58,8 +58,7 @@ fn test_splice_included_ranges() { assert_eq!(change, 0..1); // does not create overlapping ranges - let (new_ranges, change) = - splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]); + let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]); assert_eq!( new_ranges, &[ts_range(20..32), ts_range(50..60), ts_range(80..90)] @@ -104,7 +103,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { ); let mut syntax_map = SyntaxMap::new(&buffer); - syntax_map.set_language_registry(registry.clone()); + syntax_map.set_language_registry(registry); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( @@ -165,7 +164,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { // Put the vec! macro back, adding back the syntactic layer. buffer.undo(); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_layers_for_range( &syntax_map, @@ -252,8 +251,8 @@ fn test_dynamic_language_injection(cx: &mut App) { assert!(syntax_map.contains_unknown_injections()); registry.add(Arc::new(html_lang())); - syntax_map.reparse(markdown.clone(), &buffer); - syntax_map.reparse(markdown_inline.clone(), &buffer); + syntax_map.reparse(markdown, &buffer); + syntax_map.reparse(markdown_inline, &buffer); assert_layers_for_range( &syntax_map, &buffer, @@ -862,7 +861,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) { log::info!("editing"); buffer.edit_via_marked_text(&text); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_capture_ranges( &syntax_map, @@ -986,7 +985,7 @@ fn test_random_edits( syntax_map.reparse(language.clone(), &buffer); let mut reference_syntax_map = SyntaxMap::new(&buffer); - reference_syntax_map.set_language_registry(registry.clone()); + reference_syntax_map.set_language_registry(registry); log::info!("initial text:\n{}", buffer.text()); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index 1e3e12758d..11d8a070d2 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -88,11 +88,11 @@ pub fn text_diff_with_options( let new_offset = new_byte_range.start; hunk_input.clear(); hunk_input.update_before(tokenize( - &old_text[old_byte_range.clone()], + &old_text[old_byte_range], options.language_scope.clone(), )); hunk_input.update_after(tokenize( - &new_text[new_byte_range.clone()], + &new_text[new_byte_range], options.language_scope.clone(), )); diff_internal(&hunk_input, |old_byte_range, new_byte_range, _, _| { @@ -103,7 +103,7 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; edits.push((old_byte_range, replacement_text)); }); @@ -111,9 +111,9 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; - edits.push((old_byte_range.clone(), replacement_text)); + edits.push((old_byte_range, replacement_text)); } }, ); @@ -186,7 +186,7 @@ fn tokenize(text: &str, language_scope: Option) -> impl Iterator< let mut prev = None; let mut start_ix = 0; iter::from_fn(move || { - while let Some((ix, c)) = chars.next() { + for (ix, c) in chars.by_ref() { let mut token = None; let kind = classifier.kind(c); if let Some((prev_char, prev_kind)) = prev diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 979513bc96..73c142c8ca 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -96,7 +96,7 @@ impl LanguageToolchainStore for T { } type DefaultIndex = usize; -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct ToolchainList { pub toolchains: Vec, pub default: Option, diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index b10529c3d9..d5313b6a3a 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -14,7 +14,7 @@ use client::Client; use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -54,7 +54,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName = pub fn init(client: Arc, cx: &mut App) { init_settings(cx); - RefreshLlmTokenListener::register(client.clone(), cx); + RefreshLlmTokenListener::register(client, cx); } pub fn init_settings(cx: &mut App) { @@ -538,7 +538,7 @@ pub trait LanguageModel: Send + Sync { if let Some(first_event) = events.next().await { match first_event { Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => { - message_id = Some(id.clone()); + message_id = Some(id); } Ok(LanguageModelCompletionEvent::Text(text)) => { first_item_text = Some(text); @@ -640,24 +640,14 @@ pub trait LanguageModelProvider: 'static { window: &mut Window, cx: &mut App, ) -> AnyView; - fn must_accept_terms(&self, _cx: &App) -> bool { - false - } - fn render_accept_terms( - &self, - _view: LanguageModelProviderTosView, - _cx: &mut App, - ) -> Option { - None - } fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, - Other(&'static str), + Other(SharedString), } #[derive(PartialEq, Eq)] diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 0e10050dae..8a7f3456fb 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -82,7 +82,7 @@ impl LlmApiToken { let response = client.cloud_client().create_llm_token(system_id).await?; *lock = Some(response.token.0.clone()); - Ok(response.token.0.clone()) + Ok(response.token.0) } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 8f52f8c1c3..c7693a64c7 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -6,7 +6,6 @@ use collections::BTreeMap; use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*}; use std::{str::FromStr, sync::Arc}; use thiserror::Error; -use util::maybe; pub fn init(cx: &mut App) { let registry = cx.new(|_cx| LanguageModelRegistry::default()); @@ -25,9 +24,6 @@ pub enum ConfigurationError { ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc), - #[error("Using the {} LLM provider requires accepting the Terms of Service.", - .0.name().0)] - ProviderPendingTermsAcceptance(Arc), } impl std::fmt::Debug for ConfigurationError { @@ -38,9 +34,6 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } - Self::ProviderPendingTermsAcceptance(provider) => { - write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) - } } } } @@ -48,7 +41,9 @@ impl std::fmt::Debug for ConfigurationError { #[derive(Default)] pub struct LanguageModelRegistry { default_model: Option, - default_fast_model: Option, + /// This model is automatically configured by a user's environment after + /// authenticating all providers. It's only used when default_model is not available. + environment_fallback_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -104,9 +99,6 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, - InlineAssistantModelChanged, - CommitMessageModelChanged, - ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -200,12 +192,6 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } - if model.provider.must_accept_terms(cx) { - return Some(ConfigurationError::ProviderPendingTermsAcceptance( - model.provider, - )); - } - None } @@ -238,7 +224,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model, cx); + self.set_inline_assistant_model(configured_model); } pub fn select_commit_message_model( @@ -247,7 +233,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model, cx); + self.set_commit_message_model(configured_model); } pub fn select_thread_summary_model( @@ -256,7 +242,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model, cx); + self.set_thread_summary_model(configured_model); } /// Selects and sets the inline alternatives for language models based on @@ -290,68 +276,60 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model.as_ref(), model.as_ref()) { + match (self.default_model(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } - self.default_fast_model = maybe!({ - let provider = &model.as_ref()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider: provider.clone(), - model: fast_model, - }) - }); self.default_model = model; } - pub fn set_inline_assistant_model( + pub fn set_environment_fallback_model( &mut self, model: Option, cx: &mut Context, ) { - match (self.inline_assistant_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::InlineAssistantModelChanged), + if self.default_model.is_none() { + match (self.environment_fallback_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::DefaultModelChanged), + } } + self.environment_fallback_model = model; + } + + pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.commit_message_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::CommitMessageModelChanged), - } + pub fn set_commit_message_model(&mut self, model: Option) { self.commit_message_model = model; } - pub fn set_thread_summary_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.thread_summary_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::ThreadSummaryModelChanged), - } + pub fn set_thread_summary_model(&mut self, model: Option) { self.thread_summary_model = model; } + #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model.clone() + self.default_model + .clone() + .or_else(|| self.environment_fallback_model.clone()) + } + + pub fn default_fast_model(&self, cx: &App) -> Option { + let provider = self.default_model()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider, + model: fast_model, + }) } pub fn inline_assistant_model(&self) -> Option { @@ -365,7 +343,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self) -> Option { + pub fn commit_message_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -373,11 +351,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self) -> Option { + pub fn thread_summary_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -385,7 +363,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } @@ -422,4 +400,34 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } + + #[gpui::test] + async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { + let registry = cx.new(|_| LanguageModelRegistry::default()); + + let provider = FakeLanguageModelProvider::default(); + registry.update(cx, |registry, cx| { + registry.register_provider(provider.clone(), cx); + }); + + cx.update(|cx| provider.authenticate(cx)).await.unwrap(); + + registry.update(cx, |registry, cx| { + let provider = registry.provider(&provider.id()).unwrap(); + + registry.set_environment_fallback_model( + Some(ConfiguredModel { + provider: provider.clone(), + model: provider.default_model(cx).unwrap(), + }), + cx, + ); + + let default_model = registry.default_model().unwrap(); + let fallback_model = registry.environment_fallback_model.clone().unwrap(); + + assert_eq!(default_model.model.id(), fallback_model.model.id()); + assert_eq!(default_model.provider.id(), fallback_model.provider.id()); + }); + } } diff --git a/crates/language_model/src/role.rs b/crates/language_model/src/role.rs index 953dfa6fdf..4b47ef36dd 100644 --- a/crates/language_model/src/role.rs +++ b/crates/language_model/src/role.rs @@ -19,7 +19,7 @@ impl Role { } } - pub fn to_proto(&self) -> proto::LanguageModelRole { + pub fn to_proto(self) -> proto::LanguageModelRole { match self { Role::User => proto::LanguageModelRole::LanguageModelUser, Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f6..cd41478668 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 18e6f47ed0..beed306e74 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,8 +3,12 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use gpui::{App, Context, Entity}; -use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use futures::future; +use gpui::{App, AppContext as _, Context, Entity}; +use language_model::{ + AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, +}; +use project::DisableAiSettings; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -13,7 +17,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::CloudLanguageModelProvider; +use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -48,6 +52,13 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); + + let mut already_authenticated = false; + if !DisableAiSettings::get_global(cx).disable_ai { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; + } + cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -65,6 +76,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; + already_authenticated = false; + } + + if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; } }) .detach(); @@ -104,7 +121,7 @@ fn register_language_model_providers( cx: &mut Context, ) { registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + CloudLanguageModelProvider::new(user_store, client.clone(), cx), cx, ); @@ -151,3 +168,83 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } + +/// Authenticates all providers in the [`LanguageModelRegistry`]. +/// +/// We do this so that we can populate the language selector with all of the +/// models from the configured providers. +/// +/// This function won't do anything if AI is disabled. +fn authenticate_all_providers(registry: Entity, cx: &mut App) { + let providers_to_authenticate = registry + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); + + for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { + tasks.push(cx.background_spawn(async move { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + })); + } + + let all_authenticated_future = future::join_all(tasks); + + cx.spawn(async move |cx| { + all_authenticated_future.await; + + registry + .update(cx, |registry, cx| { + let cloud_provider = registry.provider(&cloud::PROVIDER_ID); + let fallback_model = cloud_provider + .iter() + .chain(registry.providers().iter()) + .find(|provider| provider.is_authenticated(cx)) + .and_then(|provider| { + Some(ConfiguredModel { + provider: provider.clone(), + model: provider + .default_model(cx) + .or_else(|| provider.recommended_models(cx).first().cloned())?, + }) + }); + registry.set_environment_fallback_model(fallback_model, cx); + }) + .ok(); + }) + .detach(); +} diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 0d061c0587..c492edeaf5 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1041,9 +1041,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", - ConfigurationViewTargetAgent::Other(agent) => agent, + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), }))) .child( List::new() diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 193d218094..178c767950 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -917,7 +917,7 @@ pub fn map_to_language_model_completion_events( Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking { ReasoningContentBlockDelta::Text(thoughts) => { Some(Ok(LanguageModelCompletionEvent::Thinking { - text: thoughts.clone(), + text: thoughts, signature: None, })) } @@ -968,7 +968,7 @@ pub fn map_to_language_model_completion_events( id: tool_use.id.into(), name: tool_use.name.into(), is_input_complete: true, - raw_input: tool_use.input_json.clone(), + raw_input: tool_use.input_json, input, }, )) @@ -1086,21 +1086,18 @@ impl ConfigurationView { .access_key_id_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let secret_access_key = self .secret_access_key_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = self .session_token_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = if session_token.is_empty() { @@ -1108,13 +1105,7 @@ impl ConfigurationView { } else { Some(session_token) }; - let region = self - .region_editor - .read(cx) - .text(cx) - .to_string() - .trim() - .to_string(); + let region = self.region_editor.read(cx).text(cx).trim().to_string(); let region = if region.is_empty() { "us-east-1".to_string() } else { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index d3fee7b63b..fb6e2fb1e4 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -23,9 +23,9 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, - ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, + PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -44,8 +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}; -const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -118,7 +118,6 @@ pub struct State { llm_api_token: LlmApiToken, user_store: Entity, status: client::Status, - accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -140,15 +139,14 @@ impl State { Self { client: client.clone(), llm_api_token: LlmApiToken::default(), - user_store: user_store.clone(), + user_store, status, - accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async move { + maybe!(async { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; @@ -197,24 +195,6 @@ impl State { state.update(cx, |_, cx| cx.notify()) }) } - - fn has_accepted_terms_of_service(&self, cx: &App) -> bool { - self.user_store.read(cx).has_accepted_terms_of_service() - } - - fn accept_terms_of_service(&mut self, cx: &mut Context) { - let user_store = self.user_store.clone(); - 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_of_service_task = None; - cx.notify() - }) - })); - } - fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context) { let mut models = Vec::new(); @@ -307,7 +287,7 @@ impl CloudLanguageModelProvider { Self { client, - state: state.clone(), + state, _maintain_client_status: maintain_client_status, } } @@ -320,7 +300,7 @@ impl CloudLanguageModelProvider { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, - llm_api_token: llm_api_token.clone(), + llm_api_token, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) @@ -384,7 +364,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) + !state.is_signed_out(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -401,112 +381,11 @@ impl LanguageModelProvider for CloudLanguageModelProvider { .into() } - fn must_accept_terms(&self, cx: &App) -> bool { - !self.state.read(cx).has_accepted_terms_of_service(cx) - } - - fn render_accept_terms( - &self, - view: LanguageModelProviderTosView, - cx: &mut App, - ) -> Option { - 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> { Task::ready(Ok(())) } } -fn render_accept_terms( - view_kind: LanguageModelProviderTosView, - 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::ThreadEmptyState); - - let terms_button = Button::new("terms_of_service", "Terms of Service") - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) - .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); - - let button_container = h_flex().child( - Button::new("accept_terms", "I accept the Terms of Service") - .when(!thread_empty_state, |this| { - this.full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - }) - .when(thread_empty_state, |this| { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - .label_size(LabelSize::Small) - }) - .disabled(accept_terms_of_service_in_progress) - .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)), - ); - - if thread_empty_state { - h_flex() - .w_full() - .flex_wrap() - .justify_between() - .child( - h_flex() - .child( - Label::new("To start using Zed AI, please read and accept the") - .size(LabelSize::Small), - ) - .child(terms_button), - ) - .child(button_container) - } else { - v_flex() - .w_full() - .gap_2() - .child( - h_flex() - .flex_wrap() - .when(thread_fresh_start, |this| this.justify_center()) - .child(Label::new( - "To start using Zed AI, please read and accept the", - )) - .child(terms_button), - ) - .child({ - match view_kind { - LanguageModelProviderTosView::TextThreadPopup => { - button_container.w_full().justify_end() - } - LanguageModelProviderTosView::Configuration => { - button_container.w_full().justify_start() - } - LanguageModelProviderTosView::ThreadFreshStart => { - button_container.w_full().justify_center() - } - LanguageModelProviderTosView::ThreadEmptyState => div().w_0(), - } - }) - } -} - pub struct CloudLanguageModel { id: LanguageModelId, model: Arc, @@ -1107,10 +986,7 @@ struct ZedAiConfiguration { plan: Option, subscription_period: Option<(DateTime, DateTime)>, eligible_for_trial: bool, - has_accepted_terms_of_service: bool, account_too_young: bool, - accept_terms_of_service_in_progress: bool, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } @@ -1176,58 +1052,30 @@ impl RenderOnce for ZedAiConfiguration { ); } - v_flex() - .gap_2() - .w_full() - .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) - }, - )) - }) - .map(|this| { - if self.has_accepted_terms_of_service && self.account_too_young { - this.child(young_account_banner).child( - Button::new("upgrade", "Upgrade to Pro") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) - .full_width() - .on_click(|_, _, cx| { - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ) - } else if self.has_accepted_terms_of_service { - this.text_sm() - .child(subscription_text) - .child(manage_subscription_buttons) - } else { - this - } - }) - .when(self.has_accepted_terms_of_service, |this| this) + v_flex().gap_2().w_full().map(|this| { + if self.account_too_young { + this.child(young_account_banner).child( + Button::new("upgrade", "Upgrade to Pro") + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) + .full_width() + .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), + ) + } else { + this.text_sm() + .child(subscription_text) + .child(manage_subscription_buttons) + } + }) } } struct ConfigurationView { state: Entity, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } impl ConfigurationView { fn new(state: Entity) -> 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| { @@ -1239,7 +1087,6 @@ impl ConfigurationView { Self { state, - accept_terms_of_service_callback, sign_in_callback, } } @@ -1255,10 +1102,7 @@ impl Render for ConfigurationView { plan: user_store.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), account_too_young: user_store.account_too_young(), - 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(), } } @@ -1283,7 +1127,6 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, - has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1292,10 +1135,7 @@ impl Component for ZedAiConfiguration { .is_some() .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))), eligible_for_trial, - has_accepted_terms_of_service, account_too_young, - accept_terms_of_service_in_progress: false, - accept_terms_of_service_callback: Arc::new(|_, _| {}), sign_in_callback: Arc::new(|_, _| {}), } .into_any_element() @@ -1306,33 +1146,30 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example( - "Not connected", - configuration(false, None, false, false, true), - ), + single_example("Not connected", configuration(false, None, false, false)), single_example( "Accept Terms of Service", - configuration(true, None, true, false, false), + configuration(true, None, true, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false, true), + configuration(true, None, false, false), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false, true), + configuration(true, None, true, false), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false, true), + configuration(true, Some(Plan::ZedFree), true, false), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false, true), + configuration(true, Some(Plan::ZedProTrial), true, false), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false, true), + configuration(true, Some(Plan::ZedPro), true, false), ), ]) .into_any_element(), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a36ce949b1..f252ab7aa3 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -12,9 +12,9 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse, - LanguageModelToolUseId, MessageContent, StopReason, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason, }; use language_model::{ LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, @@ -37,6 +37,8 @@ use util::ResultExt; use crate::AllLanguageModelSettings; use crate::ui::InstructionListItem; +use super::anthropic::ApiKey; + const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } + + pub fn api_key(cx: &mut App) -> Task> { + let credentials_provider = ::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .google + .api_url + .clone(); + + if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) { + Task::ready(Ok(ApiKey { + key, + from_env: true, + })) + } else { + cx.spawn(async move |cx| { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + + Ok(ApiKey { + key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + from_env: false, + }) + }) + } + } } impl LanguageModelProviderState for GoogleLanguageModelProvider { @@ -279,11 +308,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { fn configuration_view( &self, - _target_agent: language_model::ConfigurationViewTargetAgent, + target_agent: language_model::ConfigurationViewTargetAgent, window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx)) .into() } @@ -387,7 +416,7 @@ impl LanguageModel for GoogleLanguageModel { cx: &App, ) -> BoxFuture<'static, Result> { let model_id = self.model.request_id().to_string(); - let request = into_google(request, model_id.clone(), self.model.mode()); + let request = into_google(request, model_id, self.model.mode()); let http_client = self.http_client.clone(); let api_key = self.state.read(cx).api_key.clone(); @@ -577,7 +606,7 @@ pub fn into_google( top_k: None, }), safety_settings: None, - tools: (request.tools.len() > 0).then(|| { + tools: (!request.tools.is_empty()).then(|| { vec![google_ai::Tool { function_declarations: request .tools @@ -776,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage { struct ConfigurationView { api_key_editor: Entity, state: gpui::Entity, + target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option>, } impl ConfigurationView { - fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { + fn new( + state: gpui::Entity, + target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut Context, + ) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -810,6 +845,7 @@ impl ConfigurationView { editor.set_placeholder_text("AIzaSy...", cx); editor }), + target_agent, state, load_credentials_task, } @@ -885,7 +921,10 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:")) + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), + }))) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 7ac08f2c15..80b28a396b 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { .map(|model| { Arc::new(LmStudioLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 93844542ea..3f2d47fba3 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { .map(|model| { Arc::new(OllamaLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs index 3dee97aff6..bdb5fbe242 100644 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -37,7 +37,7 @@ impl IntoElement for InstructionListItem { let item_content = if let (Some(button_label), Some(button_link)) = (self.button_label, self.button_link) { - let link = button_link.clone(); + let link = button_link; let unique_id = SharedString::from(format!("{}-button", self.label)); h_flex() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 3285efaaef..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -406,10 +406,7 @@ impl LogStore { server_state.worktree_id = Some(worktree_id); } - if let Some(server) = server - .clone() - .filter(|_| server_state.io_logs_subscription.is_none()) - { + if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { let io_tx = self.io_tx.clone(); let server_id = server.server_id(); server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { @@ -930,7 +927,7 @@ impl LspLogView { let state = log_store.language_servers.get(&server_id)?; Some(LogMenuItem { server_id, - server_name: name.clone(), + server_name: name, server_kind: state.kind.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), @@ -1527,7 +1524,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; @@ -1595,7 +1592,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 4fe8e11f94..cf84ac34c4 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -156,7 +156,7 @@ impl SyntaxTreeView { .buffer_snapshot .range_to_buffer_ranges(selection_range) .pop()?; - let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone(); + let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap(); Some((buffer, range, excerpt_id)) })?; diff --git a/crates/languages/src/bash/overrides.scm b/crates/languages/src/bash/overrides.scm new file mode 100644 index 0000000000..81fec9a5f5 --- /dev/null +++ b/crates/languages/src/bash/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment.inclusive +(string) @string diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 999d4a74c3..2820f55a49 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -22,7 +22,7 @@ impl CLspAdapter { #[async_trait(?Send)] impl super::LspAdapter for CLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -253,8 +253,7 @@ impl super::LspAdapter for CLspAdapter { .grammar() .and_then(|g| g.highlight_id_for_name(highlight_name?)) { - let mut label = - CodeLabel::plain(label.to_string(), completion.filter_text.as_deref()); + let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); label.runs.push(( 0..label.text.rfind('(').unwrap_or(label.text.len()), highlight_id, @@ -264,10 +263,7 @@ impl super::LspAdapter for CLspAdapter { } _ => {} } - Some(CodeLabel::plain( - label.to_string(), - completion.filter_text.as_deref(), - )) + Some(CodeLabel::plain(label, completion.filter_text.as_deref())) } async fn label_for_symbol( diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index d6f9538ee4..24e2ca2f56 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -53,7 +53,7 @@ const BINARY: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl super::LspAdapter for GoLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -525,7 +525,7 @@ impl ContextProvider for GoContextProvider { }) .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy())); - (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string()) + (GO_PACKAGE_TASK_VARIABLE.clone(), package_name) }); let go_module_root_variable = local_abs_path @@ -702,7 +702,7 @@ impl ContextProvider for GoContextProvider { label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()), command: "go".into(), args: vec!["generate".into()], - cwd: package_cwd.clone(), + cwd: package_cwd, tags: vec!["go-generate".to_owned()], ..TaskTemplate::default() }, @@ -710,7 +710,7 @@ impl ContextProvider for GoContextProvider { label: "go generate ./...".into(), command: "go".into(), args: vec!["generate".into(), "./...".into()], - cwd: module_cwd.clone(), + cwd: module_cwd, ..TaskTemplate::default() }, ]))) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index 9d5ebbaf71..ebeac7efff 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,6 +231,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -250,4 +251,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index 7baba5f227..dbec1937b1 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ac653d5b2e..4fcf865568 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -488,7 +488,7 @@ impl NodeVersionAdapter { #[async_trait(?Send)] impl LspAdapter for NodeVersionAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/jsonc/overrides.scm b/crates/languages/src/jsonc/overrides.scm index cc966ad4c1..81fec9a5f5 100644 --- a/crates/languages/src/jsonc/overrides.scm +++ b/crates/languages/src/jsonc/overrides.scm @@ -1 +1,2 @@ +(comment) @comment.inclusive (string) @string diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 75289dd59d..d391e67d33 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -104,7 +104,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new()); let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone())); let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone())); - let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone())); + let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node)); let built_in_languages = [ LanguageInfo { @@ -119,12 +119,12 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "cpp", - adapters: vec![c_lsp_adapter.clone()], + adapters: vec![c_lsp_adapter], ..Default::default() }, LanguageInfo { name: "css", - adapters: vec![css_lsp_adapter.clone()], + adapters: vec![css_lsp_adapter], ..Default::default() }, LanguageInfo { @@ -146,20 +146,20 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "gowork", - adapters: vec![go_lsp_adapter.clone()], - context: Some(go_context_provider.clone()), + adapters: vec![go_lsp_adapter], + context: Some(go_context_provider), ..Default::default() }, LanguageInfo { name: "json", - adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()], + adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter], context: Some(json_context_provider.clone()), ..Default::default() }, LanguageInfo { name: "jsonc", - adapters: vec![json_lsp_adapter.clone()], - context: Some(json_context_provider.clone()), + adapters: vec![json_lsp_adapter], + context: Some(json_context_provider), ..Default::default() }, LanguageInfo { @@ -174,7 +174,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "python", - adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], + adapters: vec![python_lsp_adapter, py_lsp_adapter], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), manifest_name: Some(SharedString::new_static("pyproject.toml").into()), @@ -201,7 +201,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { LanguageInfo { name: "javascript", adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()], - context: Some(typescript_context.clone()), + context: Some(typescript_context), ..Default::default() }, LanguageInfo { @@ -277,13 +277,13 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { move || adapter.clone() }); languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), { - let adapter = vtsls_adapter.clone(); + let adapter = vtsls_adapter; move || adapter.clone() }); languages.register_available_lsp_adapter( LanguageServerName("typescript-language-server".into()), { - let adapter = typescript_lsp_adapter.clone(); + let adapter = typescript_lsp_adapter; move || adapter.clone() }, ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 6c92d78525..d21b5dabd3 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -103,7 +103,7 @@ impl PythonLspAdapter { #[async_trait(?Send)] impl LspAdapter for PythonLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( @@ -1026,7 +1026,7 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl LspAdapter for PyLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -1318,7 +1318,7 @@ impl BasedPyrightLspAdapter { #[async_trait(?Send)] impl LspAdapter for BasedPyrightLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index eb5e0cee7c..3e8dce756b 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -106,7 +106,7 @@ impl ManifestProvider for CargoManifestProvider { #[async_trait(?Send)] impl LspAdapter for RustLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn check_if_user_installed( @@ -510,20 +510,6 @@ impl LspAdapter for RustLspAdapter { } } - let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if cargo_diagnostics_fetched_separately { - let disable_check_on_save = json!({ - "checkOnSave": false, - }); - if let Some(initialization_options) = &mut original.initialization_options { - merge_json_value_into(disable_check_on_save, initialization_options); - } else { - original.initialization_options = Some(disable_check_on_save); - } - } - Ok(original) } } @@ -659,7 +645,7 @@ impl ContextProvider for RustContextProvider { .variables .get(CUSTOM_TARGET_DIR) .cloned(); - let run_task_args = if let Some(package_to_run) = package_to_run.clone() { + let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run] } else { vec!["run".into()] @@ -1019,8 +1005,8 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option path.clone(), // Tar and gzip extract in place. - AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe + AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place. + AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe }; anyhow::Ok(LanguageServerBinary { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 29a96d9515..47eb254053 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -44,7 +44,7 @@ impl TailwindLspAdapter { #[async_trait(?Send)] impl LspAdapter for TailwindLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index 5e2fbbf63a..f7cb987831 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,6 +237,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -256,4 +257,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 48da80995b..9eec01cc89 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string (string_fragment) @injection.content diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index afc84c3aff..77cf1a64f1 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -557,7 +557,7 @@ struct TypeScriptVersions { #[async_trait(?Send)] impl LspAdapter for TypeScriptLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -879,7 +879,7 @@ impl LspAdapter for EsLintLspAdapter { } fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index af37ef6415..84cbbae77d 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,6 +248,7 @@ "is" "keyof" "let" + "module" "namespace" "new" "of" @@ -272,4 +273,4 @@ "while" "with" "yield" -] @keyword \ No newline at end of file +] @keyword diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 7affdc5b75..1ca1e9ad59 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,6 +15,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index fd227e267d..f7152b0b5d 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -67,7 +67,7 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); #[async_trait(?Send)] impl LspAdapter for VtslsLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 6ac92e0b2b..b9197b12ae 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -38,7 +38,7 @@ impl YamlLspAdapter { #[async_trait(?Send)] impl LspAdapter for YamlLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/yaml/overrides.scm b/crates/languages/src/yaml/overrides.scm new file mode 100644 index 0000000000..9503051a62 --- /dev/null +++ b/crates/languages/src/yaml/overrides.scm @@ -0,0 +1,5 @@ +(comment) @comment.inclusive +[ + (single_quote_scalar) + (double_quote_scalar) +] @string diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 58059967b7..3575325ac0 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,6 +25,7 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true +audio.workspace = true gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] } gpui_tokio.workspace = true http_client_tls.workspace = true @@ -35,6 +36,7 @@ nanoid.workspace = true parking_lot.workspace = true postage.workspace = true smallvec.workspace = true +settings.workspace = true tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index 51f335c2db..7580642990 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -183,7 +183,7 @@ impl LivekitWindow { match track { livekit_client::RemoteTrack::Audio(track) => { output.audio_output_stream = Some(( - publication.clone(), + publication, room.play_remote_audio_track(&track, cx).unwrap(), )); } diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index e3934410e1..055aa3704e 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -24,8 +24,11 @@ mod livekit_client; )))] pub use livekit_client::*; -// If you need proper LSP in livekit_client you've got to comment out -// the mocks and test +// If you need proper LSP in livekit_client you've got to comment +// - the cfg blocks above +// - the mods: mock_client & test and their conditional blocks +// - the pub use mock_client::* and their conditional blocks + #[cfg(any( test, feature = "test-support", diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index adeea4f512..0751b014f4 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -1,15 +1,16 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; +use audio::AudioSettings; use collections::HashMap; use futures::{SinkExt, channel::mpsc}; use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task}; use gpui_tokio::Tokio; +use log::info; use playback::capture_local_video_track; +use settings::Settings; mod playback; -#[cfg(feature = "record-microphone")] -mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; @@ -125,9 +126,14 @@ impl Room { pub fn play_remote_audio_track( &self, track: &RemoteAudioTrack, - _cx: &App, + cx: &mut App, ) -> Result { - Ok(self.playback.play_remote_audio_track(&track.0)) + if AudioSettings::get_global(cx).rodio_audio { + info!("Using experimental.rodio_audio audio pipeline"); + playback::play_remote_audio_track(&track.0, cx) + } else { + Ok(self.playback.play_remote_audio_track(&track.0)) + } } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d1eec42f8f..d6b64dbaca 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -18,13 +18,16 @@ use livekit::webrtc::{ video_stream::native::NativeVideoStream, }; use parking_lot::Mutex; +use rodio::Source; use std::cell::RefCell; use std::sync::Weak; -use std::sync::atomic::{self, AtomicI32}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::time::Duration; use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread}; use util::{ResultExt as _, maybe}; +mod source; + pub(crate) struct AudioStack { executor: BackgroundExecutor, apm: Arc>, @@ -40,6 +43,29 @@ pub(crate) struct AudioStack { const SAMPLE_RATE: u32 = 48000; const NUM_CHANNELS: u32 = 2; +pub(crate) fn play_remote_audio_track( + track: &livekit::track::RemoteAudioTrack, + cx: &mut gpui::App, +) -> Result { + let stop_handle = Arc::new(AtomicBool::new(false)); + let stop_handle_clone = stop_handle.clone(); + let stream = source::LiveKitStream::new(cx.background_executor(), track) + .stoppable() + .periodic_access(Duration::from_millis(50), move |s| { + if stop_handle.load(Ordering::Relaxed) { + s.stop(); + } + }); + audio::Audio::play_source(stream, cx).context("Could not play audio")?; + + let on_drop = util::defer(move || { + stop_handle_clone.store(true, Ordering::Relaxed); + }); + Ok(AudioStream::Output { + _drop: Box::new(on_drop), + }) +} + impl AudioStack { pub(crate) fn new(executor: BackgroundExecutor) -> Self { let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new( @@ -61,7 +87,7 @@ impl AudioStack { ) -> AudioStream { let output_task = self.start_output(); - let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed); + let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, sample_rate: SAMPLE_RATE, @@ -97,6 +123,23 @@ impl AudioStack { } } + fn start_output(&self) -> Arc> { + if let Some(task) = self._output_task.borrow().upgrade() { + return task; + } + let task = Arc::new(self.executor.spawn({ + let apm = self.apm.clone(); + let mixer = self.mixer.clone(); + async move { + Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) + .await + .log_err(); + } + })); + *self._output_task.borrow_mut() = Arc::downgrade(&task); + task + } + pub(crate) fn capture_local_microphone_track( &self, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { @@ -117,7 +160,6 @@ impl AudioStack { let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded(); let transmit_task = self.executor.spawn({ - let source = source.clone(); async move { while let Some(frame) = frame_rx.next().await { source.capture_frame(&frame).await.log_err(); @@ -132,29 +174,12 @@ impl AudioStack { drop(transmit_task); drop(capture_task); }); - return Ok(( + Ok(( super::LocalAudioTrack(track), AudioStream::Output { _drop: Box::new(on_drop), }, - )); - } - - fn start_output(&self) -> Arc> { - if let Some(task) = self._output_task.borrow().upgrade() { - return task; - } - let task = Arc::new(self.executor.spawn({ - let apm = self.apm.clone(); - let mixer = self.mixer.clone(); - async move { - Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) - .await - .log_err(); - } - })); - *self._output_task.borrow_mut() = Arc::downgrade(&task); - task + )) } async fn play_output( diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs new file mode 100644 index 0000000000..021640247d --- /dev/null +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -0,0 +1,67 @@ +use futures::StreamExt; +use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; +use livekit::track::RemoteAudioTrack; +use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter}; + +use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE}; + +fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { + let samples = frame.data.iter().copied(); + let samples = SampleTypeConverter::<_, _>::new(samples); + let samples: Vec = samples.collect(); + SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples) +} + +pub struct LiveKitStream { + // shared_buffer: SharedBuffer, + inner: rodio::queue::SourcesQueueOutput, + _receiver_task: gpui::Task<()>, +} + +impl LiveKitStream { + pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self { + let mut stream = + NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32); + let (queue_input, queue_output) = rodio::queue::queue(true); + // spawn rtc stream + let receiver_task = executor.spawn({ + async move { + while let Some(frame) = stream.next().await { + let samples = frame_to_samplesbuffer(frame); + queue_input.append(samples); + } + } + }); + + LiveKitStream { + _receiver_task: receiver_task, + inner: queue_output, + } + } +} + +impl Iterator for LiveKitStream { + type Item = rodio::Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl Source for LiveKitStream { + fn current_span_len(&self) -> Option { + self.inner.current_span_len() + } + + fn channels(&self) -> rodio::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> rodio::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce9e2fe229..942225d098 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 862b657c8c..16c198601a 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -30,7 +30,7 @@ pub fn main() { let node_runtime = NodeRuntime::unavailable(); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - languages::init(language_registry.clone(), node_runtime, cx); + languages::init(language_registry, node_runtime, cx); theme::init(LoadThemes::JustBase, cx); Assets.load_fonts(cx).unwrap(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a161ddd074..f16da45d79 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1085,11 +1085,11 @@ impl Element for MarkdownElement { ); el.child( h_flex() - .w_5() + .w_4() .absolute() - .top_1() - .right_1() - .justify_center() + .top_1p5() + .right_1p5() + .justify_end() .child(codeblock), ) }); @@ -1115,11 +1115,12 @@ impl Element for MarkdownElement { cx, ); el.child( - div() + h_flex() + .w_4() .absolute() .top_0() .right_0() - .w_5() + .justify_end() .visible_on_hover("code_block") .child(codeblock), ) @@ -1320,11 +1321,11 @@ fn render_copy_code_block_button( ) .icon_color(Color::Muted) .icon_size(IconSize::Small) + .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ - let id = id.clone(); - let markdown = markdown.clone(); + let markdown = markdown; move |_event, _window, cx| { let id = id.clone(); markdown.update(cx, |this, cx| { diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 8c8d9e177f..b51b98a2ed 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -178,7 +178,6 @@ impl<'a> MarkdownParser<'a> { _ => None, }, Event::Rule => { - let source_range = source_range.clone(); self.cursor += 1; Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) } @@ -401,7 +400,7 @@ impl<'a> MarkdownParser<'a> { } if !text.is_empty() { markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), + source_range, contents: text, highlights, regions, @@ -420,7 +419,7 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; ParsedMarkdownHeading { - source_range: source_range.clone(), + source_range, level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index c2b98f69c8..1121d64655 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -115,8 +115,7 @@ impl MarkdownPreviewView { pane.activate_item(existing_follow_view_idx, true, true, window, cx); }); } else { - let view = - Self::create_following_markdown_view(workspace, editor.clone(), window, cx); + let view = Self::create_following_markdown_view(workspace, editor, window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs index 3ce85e6b26..a35b1ebd2e 100644 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_02/settings.rs @@ -20,14 +20,14 @@ fn replace_deprecated_settings_values( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index c32da88229..eed2c46e08 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -279,7 +279,7 @@ fn rename_context_key( new_predicate = new_predicate.replace(old_key, new_key); } if new_predicate != old_predicate { - Some((context_predicate_range, new_predicate.to_string())) + Some((context_predicate_range, new_predicate)) } else { None } diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs index 8d3261676b..46cfe2f178 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/settings.rs @@ -57,7 +57,7 @@ pub fn replace_edit_prediction_provider_setting( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs index 23a3243b82..2d763e4722 100644 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_30/settings.rs @@ -25,7 +25,7 @@ fn replace_tab_close_button_setting_key( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat @@ -51,14 +51,14 @@ fn replace_tab_close_button_setting_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs index 47f65b407d..8f83d8e39e 100644 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_03_29/settings.rs @@ -19,7 +19,7 @@ fn replace_setting_value( .nodes_for_capture_index(setting_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "hide_mouse_while_typing" { return None; diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs index 56d72836fa..37ef0e45cc 100644 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_29/settings.rs @@ -19,7 +19,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; if parent_object_name != "agent" { return None; @@ -30,7 +30,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(setting_name_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "preferred_completion_mode" { return None; diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 88e3e12f02..2180a049d0 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result Option> { + if let Some(buffer_id) = anchor.buffer_id { + self.buffer(buffer_id) + } else { + let (_, buffer, _) = self.excerpt_containing(anchor, cx)?; + Some(buffer) + } + } + // If point is at the end of the buffer, the last excerpt is returned pub fn point_to_buffer_offset( &self, @@ -2427,7 +2436,7 @@ impl MultiBuffer { cx.emit(match event { language::BufferEvent::Edited => Event::Edited { singleton_buffer_edited: true, - edited_buffer: Some(buffer.clone()), + edited_buffer: Some(buffer), }, language::BufferEvent::DirtyChanged => Event::DirtyChanged, language::BufferEvent::Saved => Event::Saved, @@ -3560,9 +3569,7 @@ impl MultiBuffer { let multi = cx.new(|_| Self::new(Capability::ReadWrite)); for (text, ranges) in excerpts { let buffer = cx.new(|cx| Buffer::local(text, cx)); - let excerpt_ranges = ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())); + let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new); multi.update(cx, |multi, cx| { multi.push_excerpts(buffer, excerpt_ranges, cx) }); @@ -4071,13 +4078,9 @@ impl MultiBufferSnapshot { buffer_end = buffer_end.min(end_buffer_offset); } - if let Some(iterator) = - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end) - { - Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1) - } else { - None - } + get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { + &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 + }) }; // Visit each metadata item. @@ -5234,15 +5237,6 @@ impl MultiBufferSnapshot { excerpt_offset += ExcerptOffset::new(offset_in_transform); }; - if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { - return Anchor { - buffer_id: Some(buffer_id), - excerpt_id: *excerpt_id, - text_anchor: buffer.anchor_at(excerpt_offset.value, bias), - diff_base_anchor, - }; - } - let mut excerpts = self .excerpts .cursor::>>(&()); @@ -5266,10 +5260,17 @@ impl MultiBufferSnapshot { text_anchor, diff_base_anchor, } - } else if excerpt_offset.is_zero() && bias == Bias::Left { - Anchor::min() } else { - Anchor::max() + let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + // TODO this is a hack, remove it + if let Some((excerpt_id, _, _)) = self.as_singleton() { + anchor.excerpt_id = *excerpt_id; + } + anchor } } @@ -6311,6 +6312,14 @@ impl MultiBufferSnapshot { }) } + pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option { + if let Some(id) = anchor.buffer_id { + return Some(id); + } + let excerpt = self.excerpt_containing(anchor..anchor)?; + Some(excerpt.buffer_id()) + } + pub fn selections_in_range<'a>( &'a self, range: &'a Range, @@ -6989,7 +6998,7 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - Some(self.buffer_id) == anchor.buffer_id + (anchor.buffer_id == None || anchor.buffer_id == Some(self.buffer_id)) && self .range .context @@ -7149,7 +7158,7 @@ impl ExcerptId { Self(usize::MAX) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as _ } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 598ee0f9cb..61b4b0520f 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -2250,11 +2250,11 @@ impl ReferenceMultibuffer { let base_buffer = diff.base_text(); let mut offset = buffer_range.start; - let mut hunks = diff + let hunks = diff .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); - while let Some(hunk) = hunks.next() { + for hunk in hunks { // Ignore hunks that are outside the excerpt range. let mut hunk_range = hunk.buffer_range.to_offset(buffer); diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 672bcf1cd9..54c49bc72a 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child({ - let mut ai_upsell_card = - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); - - ai_upsell_card.tab_index = Some({ - tab_index += 1; - tab_index - 1 - }); - - ai_upsell_card - }) + .child( + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) + .tab_index(Some({ + tab_index += 1; + tab_index - 1 + })), + ) .child(render_llm_provider_section( &mut tab_index, workspace, diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 77a70dfc8d..441d2ca4b7 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -126,7 +126,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .gap_1() .child( h_flex() - .id(name.clone()) + .id(name) .relative() .w_full() .border_2() @@ -201,7 +201,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }); } else { let appearance = *SystemAppearance::global(cx); - settings.set_theme(theme.clone(), appearance); + settings.set_theme(theme, appearance); } }); } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 60a9856abe..47dfd84894 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -104,7 +104,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { "Welcome Font Changed", type = "ui font", old = theme_settings.ui_font_family, - new = font.clone() + new = font ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); @@ -134,7 +134,7 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { "Welcome Font Changed", type = "editor font", old = theme_settings.buffer_font_family, - new = font_family.clone() + new = font_family ); theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); @@ -314,7 +314,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("ui-font-picker") .menu({ - let ui_font_picker = ui_font_picker.clone(); + let ui_font_picker = ui_font_picker; move |_window, _cx| Some(ui_font_picker.clone()) }) .trigger( @@ -378,7 +378,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("buffer-font-picker") .menu({ - let buffer_font_picker = buffer_font_picker.clone(); + let buffer_font_picker = buffer_font_picker; move |_window, _cx| Some(buffer_font_picker.clone()) }) .trigger( @@ -606,7 +606,7 @@ fn render_popular_settings_section( cx: &mut App, ) -> impl IntoElement { const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning =/= into ≠."; + "Font ligatures combine two characters into one. For example, turning != into ≠."; v_flex() .pt_6() diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 6a072b00e9..8bd65d8a27 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -206,7 +206,7 @@ impl ThemePreviewTile { sidebar_width, skeleton_height.clone(), )) - .child(Self::render_pane(seed, theme, skeleton_height.clone())) + .child(Self::render_pane(seed, theme, skeleton_height)) } fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { @@ -260,7 +260,7 @@ impl ThemePreviewTile { .overflow_hidden() .child(div().size_full().child(Self::render_editor( seed, - theme.clone(), + theme, sidebar_width, Self::SKELETON_HEIGHT_DEFAULT, ))) @@ -329,9 +329,9 @@ impl Component for ThemePreviewTile { let themes_to_preview = vec![ one_dark.clone().ok(), - one_light.clone().ok(), - gruvbox_dark.clone().ok(), - gruvbox_light.clone().ok(), + one_light.ok(), + gruvbox_dark.ok(), + gruvbox_light.ok(), ] .into_iter() .flatten() @@ -348,7 +348,7 @@ impl Component for ThemePreviewTile { div() .w(px(240.)) .h(px(180.)) - .child(ThemePreviewTile::new(one_dark.clone(), 0.42)) + .child(ThemePreviewTile::new(one_dark, 0.42)) .into_any_element(), )])] } else { @@ -362,13 +362,12 @@ impl Component for ThemePreviewTile { .gap_4() .children( themes_to_preview - .iter() - .enumerate() - .map(|(_, theme)| { + .into_iter() + .map(|theme| { div() .w(px(200.)) .h(px(140.)) - .child(ThemePreviewTile::new(theme.clone(), 0.42)) + .child(ThemePreviewTile::new(theme, 0.42)) }) .collect::>(), ) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index acf6ec434a..08be82b830 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,7 +446,6 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { - pub model: String, pub choices: Vec, pub usage: Option, } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 3f072c1425..d920b935b3 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4377,12 +4377,13 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - if let Some(buffer_id) = match_range.start.buffer_id + let snapshot = editor.buffer().read(cx).snapshot(cx); + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start) && editor.is_buffer_folded(buffer_id, cx) { return false; } - if let Some(buffer_id) = match_range.start.buffer_id + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end) && editor.is_buffer_folded(buffer_id, cx) { return false; @@ -4934,7 +4935,7 @@ impl Panel for OutlinePanel { impl Focusable for OutlinePanel { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } @@ -5172,7 +5173,7 @@ fn subscribe_for_editor_events( } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { - for (_, excerpt) in excerpts { + for excerpt in excerpts.values_mut() { excerpt.invalidate_outlines(); } } diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 658a51167b..1930f654e9 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -52,7 +52,7 @@ impl RenderOnce for PanelTab { pub fn panel_button(label: impl Into) -> ui::Button { let label = label.into(); - let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); + let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); ui::Button::new(id, label) .label_size(ui::LabelSize::Small) .icon_size(ui::IconSize::Small) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 47a0f12c06..aab0354c96 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -41,7 +41,7 @@ pub fn remote_server_dir_relative() -> &'static Path { /// # Arguments /// /// * `dir` - The path to use as the custom data directory. This will be used as the base -/// directory for all user data, including databases, extensions, and logs. +/// directory for all user data, including databases, extensions, and logs. /// /// # Returns /// diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index d05308ee71..baf0918fd6 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -85,7 +85,7 @@ where .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) .anchor(self.anchor) - .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) + .when_some(self.handle, |menu, handle| menu.with_handle(handle)) .offset(gpui::Point { x: px(0.0), y: px(-2.0), diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d365089377..295bad6e59 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -88,9 +88,18 @@ pub enum BufferStoreEvent { }, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); +impl PartialEq for ProjectTransaction { + fn eq(&self, other: &Self) -> bool { + self.0.len() == other.0.len() + && self.0.iter().all(|(buffer, transaction)| { + other.0.get(buffer).is_some_and(|t| t.id == transaction.id) + }) + } +} + impl EventEmitter for BufferStore {} impl RemoteBufferStore { @@ -168,7 +177,7 @@ impl RemoteBufferStore { .with_context(|| { format!("no worktree found for id {}", file.worktree_id) })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) + buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) as Arc); } Buffer::from_proto(replica_id, capability, state, buffer_file) @@ -591,7 +600,7 @@ impl LocalBufferStore { else { return Task::ready(Err(anyhow!("no such worktree"))); }; - self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) + self.save_local_buffer(buffer, worktree, path.path, true, cx) } fn open_buffer( @@ -845,7 +854,7 @@ impl BufferStore { ) -> Task> { match &mut self.state { BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx), + BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), } } @@ -1138,7 +1147,7 @@ impl BufferStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let payload = envelope.payload.clone(); + let payload = envelope.payload; let buffer_id = BufferId::new(payload.buffer_id)?; let ops = payload .operations diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index e826f44b7b..49a430c261 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -760,7 +760,7 @@ mod tests { &store, vec![ (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), + (server_1_id, ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Starting), (server_2_id.clone(), ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Stopped), diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 6c22468040..0bf6a0d61b 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -6,9 +6,9 @@ //! //! There are few reasons for this divide: //! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them -//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. +//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. //! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain -//! current set of breakpoints. +//! current set of breakpoints. //! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. pub mod breakpoint_store; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 00fcc7e69f..c47e5d35d5 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -192,7 +192,7 @@ impl BreakpointStore { } pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub(crate) fn unshared(&mut self, cx: &mut Context) { @@ -450,9 +450,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message.clone()); + found_bp.message = Some(log_message); } else { - breakpoint.bp.message = Some(log_message.clone()); + breakpoint.bp.message = Some(log_message); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -482,9 +482,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition.clone()); + found_bp.hit_condition = Some(hit_condition); } else { - breakpoint.bp.hit_condition = Some(hit_condition.clone()); + breakpoint.bp.hit_condition = Some(hit_condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -514,9 +514,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition.clone()); + found_bp.condition = Some(condition); } else { - breakpoint.bp.condition = Some(condition.clone()); + breakpoint.bp.condition = Some(condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -591,7 +591,7 @@ impl BreakpointStore { cx: &mut Context, ) { if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path.clone(), breakpoints); + self.breakpoints.insert(new_path, breakpoints); cx.notify(); } @@ -904,7 +904,7 @@ impl BreakpointState { } #[inline] - pub fn to_int(&self) -> i32 { + pub fn to_int(self) -> i32 { match self { BreakpointState::Enabled => 0, BreakpointState::Disabled => 1, diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 3be3192369..772ff2dcfe 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1454,7 +1454,7 @@ impl DapCommand for EvaluateCommand { variables_reference: message.variable_reference, named_variables: message.named_variables, indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference.clone(), + memory_reference: message.memory_reference, value_location_reference: None, //TODO }) } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 382e83587a..2906c32ff4 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -254,14 +254,18 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style) = + let (mut ssh_command, envs, path_style, ssh_shell) = ssh_client.read_with(cx, |ssh, _| { - let (SshArgs { arguments, envs }, path_style) = - ssh.ssh_info().context("SSH arguments not found")?; + let SshInfo { + args: SshArgs { arguments, envs }, + path_style, + shell, + } = ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, + shell, )) })??; @@ -280,6 +284,7 @@ impl DapStore { } let (program, args) = wrap_for_ssh( + &ssh_shell, &ssh_command, binary .command @@ -684,7 +689,7 @@ impl DapStore { let shutdown_id = parent_session.update(cx, |parent_session, _| { parent_session.remove_child_session_id(session_id); - if parent_session.child_session_ids().len() == 0 { + if parent_session.child_session_ids().is_empty() { Some(parent_session.session_id()) } else { None @@ -701,7 +706,7 @@ impl DapStore { cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); cx.background_spawn(async move { - if shutdown_children.len() > 0 { + if !shutdown_children.is_empty() { let _ = join_all(shutdown_children).await; } @@ -721,7 +726,7 @@ impl DapStore { downstream_client: AnyProtoClient, _: &mut Context, ) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub fn unshared(&mut self, cx: &mut Context) { diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 3e28fac8af..b2f9580f9c 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -117,7 +117,7 @@ impl DapLocator for CargoLocator { .cwd .clone() .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(None, &build_config.shell).non_interactive(); let (program, args) = builder.build( Some("cargo".into()), &build_config @@ -126,7 +126,7 @@ impl DapLocator for CargoLocator { .cloned() .take_while(|arg| arg != "--") .chain(Some("--message-format=json".to_owned())) - .collect(), + .collect::>(), ); let mut child = util::command::new_smol_command(program) .args(args) diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index a8729a8ff4..42ad64e688 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -3,6 +3,7 @@ //! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold: //! - We assume that the memory is divided into pages of a fixed size. //! - We assume that each page can be either mapped or unmapped. +//! //! These two assumptions drive the shape of the memory representation. //! In particular, we want the unmapped pages to be represented without allocating any memory, as *most* //! of the memory in a program space is usually unmapped. @@ -165,8 +166,8 @@ impl Memory { /// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page. /// - If it succeeds partially, we know # of mapped bytes. /// We might also know the # of unmapped bytes. -/// However, we're still unsure about what's *after* the unreadable region. /// +/// However, we're still unsure about what's *after* the unreadable region. /// This is where this builder comes in. It lets us track the state of figuring out contents of a single page. pub(super) struct MemoryPageBuilder { chunks: MappedPageContents, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index cd792877b6..81cb3ade2e 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1394,7 +1394,7 @@ impl Session { let breakpoint_store = self.breakpoint_store.clone(); if let Some((local, path)) = self.as_running_mut().and_then(|local| { let breakpoint = local.tmp_breakpoint.take()?; - let path = breakpoint.path.clone(); + let path = breakpoint.path; Some((local, path)) }) { local @@ -1710,7 +1710,7 @@ impl Session { this.threads = result .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))) + .map(|thread| (ThreadId(thread.id), Thread::from(thread))) .collect(); this.invalidate_command_type::(); @@ -2553,10 +2553,7 @@ impl Session { mode: Option, cx: &mut Context, ) -> Task> { - let command = DataBreakpointInfoCommand { - context: context.clone(), - mode, - }; + let command = DataBreakpointInfoCommand { context, mode }; self.request(command, |_, response, _| response.ok(), cx) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index edc6b00a7b..5cf298a8bf 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -769,7 +769,7 @@ impl GitStore { .as_ref() .and_then(|weak| weak.upgrade()) { - let conflict_set = conflict_set.clone(); + let conflict_set = conflict_set; let buffer_snapshot = buffer.read(cx).text_snapshot(); git_state.update(cx, |state, cx| { @@ -912,7 +912,7 @@ impl GitStore { return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); }; let content = match &version { - Some(version) => buffer.rope_for_version(version).clone(), + Some(version) => buffer.rope_for_version(version), None => buffer.as_rope().clone(), }; let version = version.unwrap_or(buffer.version()); @@ -1506,10 +1506,7 @@ impl GitStore { let mut update = envelope.payload; let id = RepositoryId::from_proto(update.id); - let client = this - .upstream_client() - .context("no upstream client")? - .clone(); + let client = this.upstream_client().context("no upstream client")?; let mut is_new = false; let repo = this.repositories.entry(id).or_insert_with(|| { @@ -3418,7 +3415,6 @@ impl Repository { reset_mode: ResetMode, _cx: &mut App, ) -> oneshot::Receiver> { - let commit = commit.to_string(); let id = self.id; self.send_job(None, move |git_repo, _| async move { @@ -3644,7 +3640,7 @@ impl Repository { let to_stage = self .cached_status() .filter(|entry| !entry.status.staging().is_fully_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.stage_entries(to_stage, cx) } @@ -3653,16 +3649,13 @@ impl Repository { let to_unstage = self .cached_status() .filter(|entry| entry.status.staging().has_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.unstage_entries(to_unstage, cx) } pub fn stash_all(&mut self, cx: &mut Context) -> Task> { - let to_stash = self - .cached_status() - .map(|entry| entry.repo_path.clone()) - .collect(); + let to_stash = self.cached_status().map(|entry| entry.repo_path).collect(); self.stash_entries(to_stash, cx) } diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 27b191f65f..313a1e90ad 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -369,7 +369,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -400,7 +400,7 @@ mod tests { >>>>>>> "# .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -653,7 +653,7 @@ mod tests { cx.run_until_parked(); conflict_set.update(cx, |conflict_set, _| { - assert_eq!(conflict_set.has_conflict, false); + assert!(!conflict_set.has_conflict); assert_eq!(conflict_set.snapshot.conflicts.len(), 0); }); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 4594e8d140..eee492e482 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -42,8 +42,8 @@ impl<'a> GitTraversal<'a> { // other_repo/ // .git/ // our_query.txt - let mut query = path.ancestors(); - while let Some(query) = query.next() { + let query = path.ancestors(); + for query in query { let (_, snapshot) = self .repo_root_to_snapshot .range(Path::new("")..=query) @@ -199,7 +199,7 @@ pub struct GitEntryRef<'a> { } impl GitEntryRef<'_> { - pub fn to_owned(&self) -> GitEntry { + pub fn to_owned(self) -> GitEntry { GitEntry { entry: self.entry.clone(), git_summary: self.git_summary, diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index c5a198954e..e499d4e026 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -244,7 +244,7 @@ impl ProjectItem for ImageItem { } fn project_path(&self, cx: &App) -> Option { - Some(self.project_path(cx).clone()) + Some(self.project_path(cx)) } fn is_dirty(&self) -> bool { @@ -375,7 +375,6 @@ impl ImageStore { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx.clone()); - let project_path = project_path.clone(); let load_image = self .state .open_image(project_path.path.clone(), worktree, cx); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index de6848701f..ce7a871d1a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2595,11 +2595,9 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { - let requested_kinds_set = if let Some(kinds) = self.kinds { - Some(kinds.into_iter().collect::>()) - } else { - None - }; + let requested_kinds_set = self + .kinds + .map(|kinds| kinds.into_iter().collect::>()); let language_server = cx.update(|cx| { lsp_store @@ -2739,7 +2737,7 @@ impl GetCodeActions { Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(supported_action_kinds), .. - })) => Some(supported_action_kinds.clone()), + })) => Some(supported_action_kinds), _ => capabilities.code_action_kinds, } } @@ -3446,8 +3444,7 @@ impl LspCommand for GetCodeLens { capabilities .server_capabilities .code_lens_provider - .as_ref() - .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) + .is_some() } fn to_lsp( @@ -3793,7 +3790,7 @@ impl GetDocumentDiagnostics { }, uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), }, - message: info.message.clone(), + message: info.message, } }) .collect::>(); @@ -3821,12 +3818,11 @@ impl GetDocumentDiagnostics { _ => None, }, code, - code_description: match diagnostic.code_description { - Some(code_description) => Some(CodeDescription { + code_description: diagnostic + .code_description + .map(|code_description| CodeDescription { href: Some(lsp::Url::parse(&code_description).unwrap()), }), - None => None, - }, related_information: Some(related_information), tags: Some(tags), source: diagnostic.source.clone(), @@ -4491,9 +4487,8 @@ mod tests { data: Some(json!({"detail": "test detail"})), }; - let proto_diagnostic = - GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone()) - .expect("Failed to serialize diagnostic"); + let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) + .expect("Failed to serialize diagnostic"); let start = proto_diagnostic.start.unwrap(); let end = proto_diagnostic.end.unwrap(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 12505a6a03..deebaedd74 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5,7 +5,7 @@ //! This module is split up into three distinct parts: //! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. //! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. -//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. +//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. //! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. //! //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. @@ -72,10 +72,11 @@ use lsp::{ AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, - WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, + LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, + MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, + TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -84,7 +85,7 @@ use rand::prelude::*; use rpc::{ AnyProtoClient, - proto::{FromProto, ToProto}, + proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto}, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; @@ -92,7 +93,7 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::Any, + any::{Any, TypeId}, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -917,7 +918,7 @@ impl LocalLspStore { message: params.message, actions: vec![], response_channel: tx, - lsp_name: name.clone(), + lsp_name: name, }; let _ = this.update(&mut cx, |_, cx| { @@ -2954,7 +2955,7 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.clone().is_some_and(|project_path| { + let is_active_entry = path.is_some_and(|project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) @@ -3490,6 +3491,7 @@ pub struct LspStore { pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, + running_lsp_requests: HashMap>)>, } #[derive(Debug, Default, Clone)] @@ -3499,7 +3501,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared, Arc>>>; +type CodeLensTask = Shared>, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3579,6 +3581,8 @@ struct CoreSymbol { impl LspStore { pub fn init(client: &AnyProtoClient) { + client.add_entity_request_handler(Self::handle_lsp_query); + client.add_entity_message_handler(Self::handle_lsp_query_response); client.add_entity_request_handler(Self::handle_multi_lsp_query); client.add_entity_request_handler(Self::handle_restart_language_servers); client.add_entity_request_handler(Self::handle_stop_language_servers); @@ -3758,6 +3762,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3819,6 +3824,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -3924,9 +3930,7 @@ impl LspStore { _: &mut Context, ) { match event { - ToolchainStoreEvent::ToolchainActivated { .. } => { - self.request_workspace_config_refresh() - } + ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(), } } @@ -4383,8 +4387,6 @@ impl LspStore { } } - // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s) - // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside fn is_capable_for_proto_request( &self, buffer: &Entity, @@ -4641,7 +4643,6 @@ impl LspStore { Some((file, language, raw_buffer.remote_id())) }) .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - for (file, language, buffer_id) in buffers { let worktree_id = file.worktree_id(cx); let Some(worktree) = local @@ -4683,7 +4684,6 @@ impl LspStore { cx, ) .collect::>(); - for node in nodes { let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; @@ -5235,154 +5235,130 @@ impl LspStore { pub fn definitions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDefinitions { position }; - if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { - return Task::ready(Ok(Vec::new())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDefinition( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|definitions_response| { - GetDefinitions { position }.response_from_proto( - definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let definitions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect()) + Ok(Some( + definitions_task + .await + .into_iter() + .flat_map(|(_, definitions)| definitions) + .dedup() + .collect(), + )) }) } } pub fn declarations( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDeclarations { position }; - if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { - return Task::ready(Ok(Vec::new())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDeclaration( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDeclarationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|declarations_response| { - GetDeclarations { position }.response_from_proto( - declarations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDeclarations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let declarations_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDeclarations { position }, cx, ); cx.background_spawn(async move { - Ok(declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect()) + Ok(Some( + declarations_task + .await + .into_iter() + .flat_map(|(_, declarations)| declarations) + .dedup() + .collect(), + )) }) } } @@ -5392,59 +5368,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetTypeDefinitions { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetTypeDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|type_definitions_response| { - GetTypeDefinitions { position }.response_from_proto( - type_definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetTypeDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let type_definitions_task = self.request_multiple_lsp_locally( @@ -5454,12 +5416,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect()) + Ok(Some( + type_definitions_task + .await + .into_iter() + .flat_map(|(_, type_definitions)| type_definitions) + .dedup() + .collect(), + )) }) } } @@ -5469,59 +5433,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetImplementations { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetImplementation( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetImplementationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|implementations_response| { - GetImplementations { position }.response_from_proto( - implementations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetImplementations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let implementations_task = self.request_multiple_lsp_locally( @@ -5531,12 +5481,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect()) + Ok(Some( + implementations_task + .await + .into_iter() + .flat_map(|(_, implementations)| implementations) + .dedup() + .collect(), + )) }) } } @@ -5546,59 +5498,44 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetReferences { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetReferences( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); + }; + let Some(responses) = request_task.await? else { + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetReferencesResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|references_response| { - GetReferences { position }.response_from_proto( - references_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) - .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + let locations = join_all(responses.payload.into_iter().map(|lsp_response| { + GetReferences { position }.response_from_proto( + lsp_response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) + .await + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(); + Ok(Some(locations)) }) } else { let references_task = self.request_multiple_lsp_locally( @@ -5608,12 +5545,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect()) + Ok(Some( + references_task + .await + .into_iter() + .flat_map(|(_, references)| references) + .dedup() + .collect(), + )) }) } } @@ -5624,82 +5563,67 @@ impl LspStore { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeActions { range: range.clone(), kinds: kinds.clone(), }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeActions( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetCodeActionsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|code_actions_response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - code_actions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + } + .response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect(), + )) }) } else { let all_actions_task = self.request_multiple_lsp_locally( buffer, Some(range.start), - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - }, + GetCodeActions { range, kinds }, cx, ); cx.background_spawn(async move { - Ok(all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect()) + Ok(Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect(), + )) }) } } @@ -5724,8 +5648,10 @@ impl LspStore { != cached_data.lens.keys().copied().collect() }); if !has_different_servers { - return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) - .shared(); + return Task::ready(Ok(Some( + cached_data.lens.values().flatten().cloned().collect(), + ))) + .shared(); } } @@ -5763,17 +5689,19 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens.clone()); - } else if !lsp_data - .lens_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens.clone(); + if let Some(fetched_lens) = fetched_lens { + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens; + } } lsp_data.update = None; - lsp_data.lens.values().flatten().cloned().collect() + Some(lsp_data.lens.values().flatten().cloned().collect()) }) .map_err(Arc::new) }) @@ -5786,64 +5714,40 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> Task>>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeLens; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeLens( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_lsp_store, cx| { let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(HashMap::default()); + return Ok(None); }; - let responses = request_task.await?.responses; - let code_lens_actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| { - let response = match lsp_response.response? { - proto::lsp_response::Response::GetCodeLensResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }?; - let server_id = LanguageServerId::from_proto(lsp_response.server_id); - Some((server_id, response)) - }) - .map(|(server_id, code_lens_response)| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - server_id, - GetCodeLens - .response_from_proto( - code_lens_response, - lsp_store, - buffer, - cx, - ) - .await, - ) - } - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + + let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + LanguageServerId::from_proto(response.server_id), + GetCodeLens + .response_from_proto(response.response, lsp_store, buffer, cx) + .await, + ) + } + })) .await; let mut has_errors = false; @@ -5862,14 +5766,14 @@ impl LspStore { !has_errors || !code_lens_actions.is_empty(), "Failed to fetch code lens" ); - Ok(code_lens_actions) + Ok(Some(code_lens_actions)) }) } else { let code_lens_actions_task = self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn( - async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, - ) + cx.background_spawn(async move { + Ok(Some(code_lens_actions_task.await.into_iter().collect())) + }) } } @@ -6485,48 +6389,23 @@ impl LspStore { let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request( - &buffer, - &GetDocumentDiagnostics { - previous_result_id: None, - }, - cx, - ) { + let request = GetDocumentDiagnostics { + previous_result_id: None, + }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics( - proto::GetDocumentDiagnostics { - project_id: upstream_project_id, - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - }, - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); cx.background_spawn(async move { - let _proto_responses = request_task - .await? - .responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .collect::>(); // Proto requests cause the diagnostics to be pulled from language server(s) on the local side // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. // Do not attempt to further process the dummy responses here. + let _response = request_task.await?; Ok(None) }) } else { @@ -6811,16 +6690,18 @@ impl LspStore { .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors.clone()); - lsp_data.cache_version += 1; - } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors.clone(); - lsp_data.cache_version += 1; + if let Some(fetched_colors) = fetched_colors { + if lsp_data.colors_for_version == query_version_queried_for { + lsp_data.colors.extend(fetched_colors); + lsp_data.cache_version += 1; + } else if !lsp_data + .colors_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.colors_for_version = query_version_queried_for; + lsp_data.colors = fetched_colors; + lsp_data.cache_version += 1; + } } lsp_data.colors_update = None; let colors = lsp_data @@ -6845,56 +6726,45 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> Task>>>> { if let Some((client, project_id)) = self.upstream_client() { let request = GetDocumentColor {}; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); + return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { + let request_task = client.request_lsp( project_id, - buffer_id: buffer.read(cx).remote_id().to_proto(), - version: serialize_version(&buffer.read(cx).version()), - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentColor( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); - cx.spawn(async move |project, cx| { - let Some(project) = project.upgrade() else { - return Ok(HashMap::default()); + cx.spawn(async move |lsp_store, cx| { + let Some(project) = lsp_store.upgrade() else { + return Ok(None); }; let colors = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentColorResponse(response) => { - Some(( - LanguageServerId::from_proto(lsp_response.server_id), - response, - )) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|(server_id, color_response)| { + .map(|color_response| { let response = request.response_from_proto( - color_response, + color_response.response, project.clone(), buffer.clone(), cx.clone(), ); - async move { (server_id, response.await.log_err().unwrap_or_default()) } + async move { + ( + LanguageServerId::from_proto(color_response.server_id), + response.await.log_err().unwrap_or_default(), + ) + } }), ) .await @@ -6905,23 +6775,25 @@ impl LspStore { .extend(colors); acc }); - Ok(colors) + Ok(Some(colors)) }) } else { let document_colors_task = self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.background_spawn(async move { - Ok(document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect()) + Ok(Some( + document_colors_task + .await + .into_iter() + .fold(HashMap::default(), |mut acc, (server_id, colors)| { + acc.entry(server_id) + .or_insert_with(HashSet::default) + .extend(colors); + acc + }) + .into_iter() + .collect(), + )) }) } } @@ -6931,49 +6803,34 @@ impl LspStore { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetSignatureHelp { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Vec::new()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let signatures = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetSignatureHelpResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|signature_response| { + .map(|response| { let response = GetSignatureHelp { position }.response_from_proto( - signature_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -6984,7 +6841,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(signatures) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6994,11 +6852,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect::>(), + ) }) } } @@ -7008,47 +6868,32 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task> { + ) -> Task>> { if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetHover { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Vec::new()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetHover( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let hovers = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetHoverResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|hover_response| { + .map(|response| { let response = GetHover { position }.response_from_proto( - hover_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -7065,7 +6910,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(hovers) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -7075,11 +6921,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) + .collect::>(), + ) }) } } @@ -7221,7 +7069,7 @@ impl LspStore { worktree = tree; path = rel_path; } else { - worktree = source_worktree.clone(); + worktree = source_worktree; path = relativize_path(&result.worktree_abs_path, &abs_path); } @@ -7740,19 +7588,16 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .get_diagnostics(server_id) - .into_iter() - .flat_map(|diag| { - diag.iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } - }) + .buffer_diagnostics(Some(server_id)) + .iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } }) .collect::>(); @@ -8142,6 +7987,203 @@ impl LspStore { })? } + async fn handle_lsp_query( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + use proto::lsp_query::Request; + let sender_id = envelope.original_sender_id().unwrap_or_default(); + let lsp_query = envelope.payload; + let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); + match lsp_query.request.context("invalid LSP query request")? { + Request::GetReferences(get_references) => { + let position = get_references.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_references, + position, + cx.clone(), + ) + .await?; + } + Request::GetDocumentColor(get_document_color) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_document_color, + None, + cx.clone(), + ) + .await?; + } + Request::GetHover(get_hover) => { + let position = get_hover.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_hover, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeActions(get_code_actions) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_actions, + None, + cx.clone(), + ) + .await?; + } + Request::GetSignatureHelp(get_signature_help) => { + let position = get_signature_help + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_signature_help, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeLens(get_code_lens) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_lens, + None, + cx.clone(), + ) + .await?; + } + Request::GetDefinition(get_definition) => { + let position = get_definition.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetDeclaration(get_declaration) => { + let position = get_declaration + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_declaration, + position, + cx.clone(), + ) + .await?; + } + Request::GetTypeDefinition(get_type_definition) => { + let position = get_type_definition + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_type_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetImplementation(get_implementation) => { + let position = get_implementation + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_implementation, + position, + cx.clone(), + ) + .await?; + } + // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. + Request::GetDocumentDiagnostics(get_document_diagnostics) => { + let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; + let version = deserialize_version(get_document_diagnostics.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if ::ProtoRequest::stop_previous_requests( + ) || buffer.read(cx).version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let diagnostics_pull = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics_for_buffer(buffer, cx) + }) + .ok(); + if let Some(diagnostics_pull) = diagnostics_pull { + match diagnostics_pull.await { + Ok(()) => {} + Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), + }; + } + }), + ); + })?; + } + } + Ok(proto::Ack {}) + } + + async fn handle_lsp_query_response( + lsp_store: Entity, + envelope: TypedEnvelope, + cx: AsyncApp, + ) -> Result<()> { + lsp_store.read_with(&cx, |lsp_store, _| { + if let Some((upstream_client, _)) = lsp_store.upstream_client() { + upstream_client.handle_lsp_response(envelope.clone()); + } + })?; + Ok(()) + } + + // todo(lsp) remove after Zed Stable hits v0.204.x async fn handle_multi_lsp_query( lsp_store: Entity, envelope: TypedEnvelope, @@ -8715,7 +8757,7 @@ impl LspStore { (root_path.join(&old_path), root_path.join(&new_path)) }; - Self::will_rename_entry( + let _transaction = Self::will_rename_entry( this.downgrade(), worktree_id, &old_abs_path, @@ -8984,13 +9026,22 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) - .map(|path| make_text_document_identifier(&path)) + let buffer_id = envelope + .payload + .buffer_id + .map(|id| BufferId::new(id)) + .transpose()?; + buffer_id + .and_then(|buffer_id| { + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| { + Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) + }) + .map(|path| make_text_document_identifier(&path)) + }) .transpose()? } else { None @@ -9177,7 +9228,7 @@ impl LspStore { new_path: &Path, is_dir: bool, cx: AsyncApp, - ) -> Task<()> { + ) -> Task { let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); cx.spawn(async move |cx| { @@ -9210,7 +9261,7 @@ impl LspStore { .log_err() .flatten()?; - LocalLspStore::deserialize_workspace_edit( + let transaction = LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, @@ -9218,8 +9269,8 @@ impl LspStore { cx, ) .await - .ok(); - Some(()) + .ok()?; + Some(transaction) } }); tasks.push(apply_edit); @@ -9229,11 +9280,17 @@ impl LspStore { }) .ok() .flatten(); + let mut merged_transaction = ProjectTransaction::default(); for task in tasks { // Await on tasks sequentially so that the order of application of edits is deterministic // (at least with regards to the order of registration of language servers) - task.await; + if let Some(transaction) = task.await { + for (buffer, buffer_transaction) in transaction.0 { + merged_transaction.0.insert(buffer, buffer_transaction); + } + } } + merged_transaction }) } @@ -10338,7 +10395,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| status.name.clone()) + .map(|status| status.name) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) @@ -10883,7 +10940,7 @@ impl LspStore { // Find all worktrees that have this server in their language server tree for (worktree_id, servers) in &local.lsp_tree.instances { if *worktree_id != key.worktree_id { - for (_, server_map) in &servers.roots { + for server_map in servers.roots.values() { if server_map.contains_key(&key.name) { worktrees_using_server.push(*worktree_id); } @@ -11646,12 +11703,11 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "workspace/fileOperations" => { if let Some(options) = reg.register_options { @@ -11675,12 +11731,11 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11695,36 +11750,32 @@ impl LspStore { } } "textDocument/formatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/rename" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/inlayHint" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/documentSymbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/codeAction" => { if let Some(options) = reg @@ -11740,12 +11791,11 @@ impl LspStore { } } "textDocument/definition" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/completion" => { if let Some(caps) = reg @@ -11853,7 +11903,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12004,7 +12054,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); @@ -12017,6 +12067,88 @@ impl LspStore { Ok(()) } + async fn query_lsp_locally( + lsp_store: Entity, + sender_id: proto::PeerId, + lsp_request_id: LspRequestId, + proto_request: T::ProtoRequest, + position: Option, + mut cx: AsyncApp, + ) -> Result<()> + where + T: LspCommand + Clone, + T::ProtoRequest: proto::LspRequestMessage, + ::Response: + Into<::Response>, + { + let buffer_id = BufferId::new(proto_request.buffer_id())?; + let version = deserialize_version(proto_request.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let request = + T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let request_task = + lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if T::ProtoRequest::stop_previous_requests() + || buffer_version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let response = request_task.await; + lsp_store + .update(cx, |lsp_store, cx| { + if let Some((client, project_id)) = lsp_store.downstream_client.clone() + { + let response = response + .into_iter() + .map(|(server_id, response)| { + ( + server_id.to_proto(), + T::response_to_proto( + response, + lsp_store, + sender_id, + &buffer_version, + cx, + ) + .into(), + ) + }) + .collect::>(); + match client.send_lsp_response::( + project_id, + lsp_request_id, + response, + ) { + Ok(()) => {} + Err(e) => { + log::error!("Failed to send LSP response: {e:#}",) + } + } + } + }) + .ok(); + }), + ); + })?; + Ok(()) + } + fn take_text_document_sync_options( capabilities: &mut lsp::ServerCapabilities, ) -> lsp::TextDocumentSyncOptions { @@ -12030,16 +12162,22 @@ impl LspStore { None => lsp::TextDocumentSyncOptions::default(), } } + + #[cfg(any(test, feature = "test-support"))] + pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { + let data = self.lsp_code_lens.get_mut(&buffer_id)?; + Some(data.update.take()?.1) + } } // Registration with registerOptions as null, should fallback to true. // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> anyhow::Result>> { +) -> Result> { Ok(match reg.register_options { - Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), - None => Some(OneOf::Left(true)), + Some(options) => OneOf::Right(serde_json::from_value::(options)?), + None => OneOf::Left(true), }) } @@ -12275,11 +12413,10 @@ async fn populate_labels_for_completions( let lsp_completions = new_completions .iter() .filter_map(|new_completion| { - if let Some(lsp_completion) = new_completion.source.lsp_completion(true) { - Some(lsp_completion.into_owned()) - } else { - None - } + new_completion + .source + .lsp_completion(true) + .map(|lsp_completion| lsp_completion.into_owned()) }) .collect::>(); @@ -12299,11 +12436,7 @@ async fn populate_labels_for_completions( for completion in new_completions { match completion.source.lsp_completion(true) { Some(lsp_completion) => { - let documentation = if let Some(docs) = lsp_completion.documentation.clone() { - Some(docs.into()) - } else { - None - }; + let documentation = lsp_completion.documentation.clone().map(|docs| docs.into()); let mut label = labels.next().flatten().unwrap_or_else(|| { CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) @@ -12691,7 +12824,7 @@ impl DiagnosticSummary { } pub fn to_proto( - &self, + self, language_server_id: LanguageServerId, path: &Path, ) -> proto::DiagnosticSummary { @@ -13159,10 +13292,10 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { let mut offset_map = vec![0; label.text.len() + 1]; let mut last_char_was_space = false; let mut new_idx = 0; - let mut chars = label.text.char_indices().fuse(); + let chars = label.text.char_indices().fuse(); let mut newlines_removed = false; - while let Some((idx, c)) = chars.next() { + for (idx, c) in chars { offset_map[idx] = new_idx; match c { diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index 274b1b8980..b02f68dd4d 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -58,7 +58,7 @@ pub fn register_notifications( language_server .on_notification::({ - let adapter = adapter.clone(); + let adapter = adapter; let this = lsp_store; move |params: InactiveRegionsParams, cx| { diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 6c425717a8..54f63220b1 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,8 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, Entity, Task, WeakEntity}; -use language::ServerHealth; -use lsp::{LanguageServer, LanguageServerName}; +use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; +use language::{Buffer, ServerHealth}; +use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -34,7 +34,6 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: language_server .on_notification::({ - let name = name.clone(); move |params, cx| { let message = params.message; let log_message = message.as_ref().map(|message| { @@ -84,31 +83,32 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -131,28 +131,33 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { + let buffer_id = buffer + .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) + .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -183,31 +188,32 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -227,3 +233,40 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } + +fn find_rust_analyzer_server( + project: &Entity, + buffer: Option<&Entity>, + cx: &mut AsyncApp, +) -> Option { + project + .read_with(cx, |project, cx| { + buffer + .and_then(|buffer| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + }) + // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup + // and use project's rust-analyzer if it's the only one. + .or_else(|| { + let rust_analyzer_servers = project + .lsp_store() + .read(cx) + .language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + if server_status.name == RUST_ANALYZER_NAME { + Some(*server_id) + } else { + None + } + }) + .collect::>(); + if rust_analyzer_servers.len() == 1 { + rust_analyzer_servers.first().copied() + } else { + None + } + }) + }) + .ok()? +} diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index ced9b34d93..5a3c7bd40f 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -77,7 +77,7 @@ impl ManifestTree { _subscriptions: [ cx.subscribe(&worktree_store, Self::on_worktree_store_event), cx.observe_global::(|this, cx| { - for (_, roots) in &mut this.root_points { + for roots in this.root_points.values_mut() { roots.update(cx, |worktree_roots, _| { worktree_roots.roots = RootPathTrie::new(); }) diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 16110463ac..9cebfda25c 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -22,9 +22,9 @@ pub(super) struct RootPathTrie