diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index 1bf6c80e40..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 6d8e0107e9..0ee6af8a1d 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -19,27 +19,14 @@ 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 d400905b4d..d8eaa6019e 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: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a34833d0fd..f4ba227168 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: - - namespace-profile-2x4-ubuntu-2404 + - ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +237,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest 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-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Environment Setup run: | @@ -458,7 +458,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - job_spec - style @@ -784,7 +784,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] 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 3f84179278..15c82643ae 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: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 2026ee7b73..5d63c34edd 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-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -248,7 +248,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} @@ -290,7 +290,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - bundle-mac - bundle-linux-x86 diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index 5dbfc9cb7f..c32a433e46 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: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..6063530e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,26 +39,6 @@ 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" @@ -191,12 +171,11 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.31" +version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" +checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" dependencies = [ "anyhow", - "async-broadcast", "futures 0.3.31", "log", "parking_lot", @@ -285,10 +264,10 @@ 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", @@ -403,7 +382,6 @@ dependencies = [ "parking_lot", "paths", "picker", - "postage", "pretty_assertions", "project", "prompt_store", @@ -443,6 +421,24 @@ 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" @@ -858,7 +854,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "extension", "futures 0.3.31", "gpui", @@ -921,7 +917,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more", + "derive_more 0.99.19", "gpui", "icons", "indoc", @@ -958,7 +954,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more", + "derive_more 0.99.19", "diffy", "editor", "feature_flags", @@ -3071,7 +3067,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more", + "derive_more 0.99.19", "feature_flags", "fs", "futures 0.3.31", @@ -3503,7 +3499,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more", + "derive_more 0.99.19", "gpui", "workspace-hack", ] @@ -4054,7 +4050,6 @@ dependencies = [ name = "crashes" version = "0.1.0" dependencies = [ - "bincode", "crash-handler", "log", "mach2 0.5.0", @@ -4064,7 +4059,6 @@ dependencies = [ "serde", "serde_json", "smol", - "system_specs", "workspace-hack", ] @@ -4666,6 +4660,27 @@ 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" @@ -4686,6 +4701,7 @@ dependencies = [ "component", "ctor", "editor", + "futures 0.3.31", "gpui", "indoc", "language", @@ -5722,10 +5738,14 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ + "client", "editor", "gpui", + "human_bytes", "menu", - "system_specs", + "release_channel", + "serde", + "sysinfo", "ui", "urlencoding", "util", @@ -6401,7 +6421,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "git2", "gpui", @@ -7431,7 +7451,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more", + "derive_more 0.99.19", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7519,7 +7539,6 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ - "anyhow", "gpui", "tokio", "util", @@ -7956,7 +7975,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -8468,7 +8487,6 @@ dependencies = [ "theme", "ui", "util", - "util_macros", "workspace", "workspace-hack", "zed_actions", @@ -11615,12 +11633,6 @@ 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" @@ -13521,7 +13533,6 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", @@ -14361,10 +14372,12 @@ 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", ] @@ -16140,21 +16153,6 @@ 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" @@ -16448,7 +16446,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.19", "fs", "futures 0.3.31", "gpui", @@ -19789,6 +19787,7 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", + "bincode", "call", "client", "clock", @@ -19807,7 +19806,6 @@ dependencies = [ "node_runtime", "parking_lot", "postage", - "pretty_assertions", "project", "remote", "schemars", @@ -19963,6 +19961,7 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", + "schemars", "scopeguard", "sea-orm", "sea-query-binder", @@ -20398,7 +20397,6 @@ dependencies = [ name = "zed" version = "0.202.0" dependencies = [ - "acp_tools", "activity_indicator", "agent", "agent_servers", @@ -20414,7 +20412,6 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", - "bincode", "breadcrumbs", "call", "channel", @@ -20513,7 +20510,6 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", - "system_specs", "tab_switcher", "task", "tasks_ui", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..b13795e1e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "crates/acp_tools", "crates/acp_thread", "crates/action_log", "crates/activity_indicator", @@ -156,7 +155,6 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", - "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -228,7 +226,6 @@ 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" } @@ -384,7 +381,6 @@ 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" } @@ -426,7 +422,8 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = "0.0.31" +agentic-coding-protocol = "0.0.10" +agent-client-protocol = "0.0.30" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" @@ -453,7 +450,6 @@ 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" } @@ -497,7 +493,6 @@ 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" @@ -537,7 +532,6 @@ 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" } diff --git a/Procfile.web b/Procfile.web deleted file mode 100644 index 8140555144..0000000000 --- a/Procfile.web +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index f923a3c7c8..0000000000 --- a/assets/icons/attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index aba193930b..bca13f8d56 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg deleted file mode 100644 index 4241d766ac..0000000000 --- a/assets/icons/pencil_unavailable.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg deleted file mode 100644 index 7d0d0e068e..0000000000 --- a/assets/icons/terminal_ghost.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index 773f5e7fa7..efd5908a90 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg deleted file mode 100644 index 8ebff8e1bc..0000000000 --- a/assets/images/acp_grid.svg +++ /dev/null @@ -1,1257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg deleted file mode 100644 index efaa46707b..0000000000 --- a/assets/images/acp_logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg deleted file mode 100644 index 6bc359cf82..0000000000 --- a/assets/images/acp_logo_serif.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c00..955e68f5a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,6 +16,7 @@ "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", @@ -40,7 +41,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", + "ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +121,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-shift-e": "editor::ToggleEditPrediction", + "ctrl-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } @@ -855,7 +856,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": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1194,16 +1195,9 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-enter": "onboarding::Finish", - "alt-shift-l": "onboarding::SignIn", + "ctrl-escape": "onboarding::Finish", + "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/default-macos.json b/assets/keymaps/default-macos.json index e72f4174ff..8b18299a91 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,12 +1301,5 @@ "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/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index c7a6c3149c..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1260 +0,0 @@ -[ - // Standard Windows bindings - { - "use_key_equivalents": true, - "bindings": { - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "ctrl-n": "menu::SelectNext", - "tab": "menu::SelectNext", - "down": "menu::SelectNext", - "ctrl-p": "menu::SelectPrevious", - "shift-tab": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "shift-alt-enter": "menu::Restart", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], - "ctrl-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "open": "workspace::Open", - "ctrl-o": "workspace::Open", - "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], - "ctrl-,": "zed::OpenSettings", - "ctrl-q": "zed::Quit", - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "ctrl-shift-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f7": "debugger::StepOver", - "ctrl-f11": "debugger::StepInto", - "shift-f11": "debugger::StepOut", - "f11": "zed::ToggleFullScreen", - "ctrl-shift-i": "edit_prediction::ToggleMenu", - "shift-alt-l": "lsp_tool::ToggleMenu" - } - }, - { - "context": "Picker || menu", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cut": "editor::Cut", - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "copy": "editor::Copy", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "paste": "editor::Paste", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "undo": "editor::Undo", - "ctrl-z": "editor::Undo", - "redo": "editor::Redo", - "ctrl-y": "editor::Redo", - "ctrl-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::LineUp", - "ctrl-down": "editor::LineDown", - "pageup": "editor::MovePageUp", - "alt-pageup": "editor::PageUp", - "shift-pageup": "editor::SelectPageUp", - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "down": "editor::MoveDown", - "pagedown": "editor::MovePageDown", - "alt-pagedown": "editor::PageDown", - "shift-pagedown": "editor::SelectPageDown", - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-left": "editor::MoveToPreviousWordStart", - "ctrl-right": "editor::MoveToNextWordEnd", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-left": "editor::SelectToPreviousWordStart", - "ctrl-shift-right": "editor::SelectToNextWordEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-a": "editor::SelectAll", - "ctrl-l": "editor::SelectLine", - "shift-alt-f": "editor::Format", - "shift-alt-o": "editor::OrganizeImports", - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-alt-space": "editor::ShowCharacterPalette", - "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-'": "editor::ToggleSelectedDiffHunks", - "ctrl-\"": "editor::ExpandAllDiffHunks", - "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "git::Blame", - "alt-g m": "git::OpenModifiedFiles", - "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "assistant::QuoteSelection", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-alt-e": "editor::SelectEnclosingSymbol", - "ctrl-shift-backspace": "editor::GoToPreviousChange", - "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "assistant::Assist", - "ctrl-s": "workspace::Save", - "save": "workspace::Save", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "ctrl-k c": "assistant::CopyCode", - "ctrl-g": "search::SelectNextMatch", - "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewThread", - "shift-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "shift-alt-c": "agent::OpenSettings", - "shift-alt-p": "agent::OpenRulesLibrary", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-alt-/": "agent::ToggleModelSelector", - "ctrl-shift-a": "agent::ToggleContextPicker", - "ctrl-shift-j": "agent::ToggleNavigationMenu", - "ctrl-shift-i": "agent::ToggleOptionsMenu", - // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "assistant::QuoteSelection", - "shift-alt-e": "agent::RemoveAllContext", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && prompt_editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && external_agent_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-enter": "agent::ChatWithFollow", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "enter": "editor::Newline", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "EditMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AgentFeedbackMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "ContextStrip", - "use_key_equivalents": true, - "bindings": { - "up": "agent::FocusUp", - "right": "agent::FocusRight", - "left": "agent::FocusLeft", - "down": "agent::FocusDown", - "backspace": "agent::RemoveFocusedContext", - "enter": "agent::AcceptSuggestedContext" - } - }, - { - "context": "AcpThread > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "PromptLibrary", - "use_key_equivalents": true, - "bindings": { - "new": "rules_library::NewRule", - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule" - } - }, - { - "context": "BufferSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPreviousMatch", - "alt-enter": "search::SelectAllMatches", - "find": "search::FocusSearch", - "ctrl-f": "search::FocusSearch", - "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "ProjectSearchBar > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", - "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", - "back": "pane::GoBack", - "alt--": "pane::GoBack", - "alt-=": "pane::GoForward", - "forward": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", - "ctrl-shift-f": "project_search::ToggleFocus", - "shift-alt-h": "search::ToggleReplace", - "alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", - "alt-f": "project_search::ToggleFilters", - "alt-r": "search::ToggleRegex", - // "ctrl-shift-alt-x": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "editor::Outdent", - "ctrl-]": "editor::Indent", - "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below - "ctrl-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "shift-alt-up": "editor::DuplicateLineUp", - "shift-alt-down": "editor::DuplicateLineDown", - "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection - "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection - "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection - "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word - "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch - "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch - "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch - "ctrl-k ctrl-i": "editor::Hover", - "ctrl-k ctrl-b": "editor::BlameHover", - "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], - "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "f2": "editor::Rename", - "f12": "editor::GoToDefinition", - "alt-f12": "editor::GoToDefinitionSplit", - "ctrl-shift-f10": "editor::GoToDefinitionSplit", - "ctrl-f12": "editor::GoToImplementation", - "shift-f12": "editor::GoToTypeDefinition", - "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", - "shift-alt-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains - "ctrl-shift-\\": "editor::MoveToEnclosingBracket", - "ctrl-shift-[": "editor::Fold", - "ctrl-shift-]": "editor::UnfoldLines", - "ctrl-k ctrl-l": "editor::ToggleFold", - "ctrl-k ctrl-[": "editor::FoldRecursive", - "ctrl-k ctrl-]": "editor::UnfoldRecursive", - "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1], - "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2], - "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3], - "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4], - "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5], - "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6], - "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7], - "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8], - "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9], - "ctrl-k ctrl-0": "editor::FoldAll", - "ctrl-k ctrl-j": "editor::UnfoldAll", - "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowWordCompletions", - "ctrl-.": "editor::ToggleCodeActions", - "ctrl-k r": "editor::RevealInFileManager", - "ctrl-k p": "editor::CopyPath", - "ctrl-\\": "pane::SplitRight", - "ctrl-shift-alt-c": "editor::DisplayCursorNames", - "alt-.": "editor::GoToHunk", - "alt-,": "editor::GoToPreviousHunk" - } - }, - { - "context": "Editor && extension == md", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "markdown::OpenPreviewToTheSide", - "ctrl-shift-v": "markdown::OpenPreview" - } - }, - { - "context": "Editor && extension == svg", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // Change the default action on `menu::Confirm` by setting the parameter - // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], - "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "shift-alt-b": "branches::OpenRecent", - "shift-alt-enter": "toast::RunAction", - "ctrl-shift-`": "workspace::NewTerminal", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", - "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", - "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::CloseAllDocks", - "alt-r": "workspace::ResetActiveDockSize", - // For 0px parameter, uses UI font size value. - "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "shift-alt-0": "workspace::ResetOpenDocksSize", - "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "shift-find": "pane::DeploySearch", - "ctrl-shift-f": "pane::DeploySearch", - "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymapEditor", - "ctrl-k ctrl-t": "theme_selector::Toggle", - "ctrl-alt-super-p": "settings_profile_selector::Toggle", - "ctrl-t": "project_symbols::Toggle", - "ctrl-p": "file_finder::Toggle", - "ctrl-tab": "tab_switcher::Toggle", - "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-e": "file_finder::Toggle", - "f1": "command_palette::Toggle", - "ctrl-shift-p": "command_palette::Toggle", - "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "ctrl-shift-d": "debug_panel::ToggleFocus", - "ctrl-shift-/": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "ctrl-k ctrl-up": "workspace::ActivatePaneUp", - "ctrl-k ctrl-down": "workspace::ActivatePaneDown", - "ctrl-k shift-left": "workspace::SwapPaneLeft", - "ctrl-k shift-right": "workspace::SwapPaneRight", - "ctrl-k shift-up": "workspace::SwapPaneUp", - "ctrl-k shift-down": "workspace::SwapPaneDown", - "ctrl-shift-x": "zed::Extensions", - "ctrl-shift-r": "task::Rerun", - "alt-t": "task::Rerun", - "shift-alt-t": "task::Spawn", - "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }], - // also possible to spawn tasks by name: - // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] - // or by tag: - // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], - "f5": "debugger::Rerun", - "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } - }, - { - "context": "Workspace && debugger_running", - "use_key_equivalents": true, - "bindings": { - "f5": "zed::NoAction" - } - }, - { - "context": "Workspace && debugger_stopped", - "use_key_equivalents": true, - "bindings": { - "f5": "debugger::Continue" - } - }, - { - "context": "ApplicationMenu", - "use_key_equivalents": true, - "bindings": { - "f10": "menu::Cancel", - "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", - "ctrl-shift-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "shift-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "shift-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "ctrl-k up": "pane::SplitUp", - "ctrl-k down": "pane::SplitDown", - "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } - }, - // Bindings that should be unified with bindings for more general actions - { - "context": "Editor && renaming", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } - }, - // Bindings for accepting edit predictions - // - // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is - // because alt-tab may not be available, as it is often used for window switching. - { - "context": "Editor && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && showing_code_actions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "up": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext", - "down": "editor::ContextMenuNext", - "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", - // Only available in debug builds: opens an element inspector for development. - "shift-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "shift-enter": "editor::ExpandExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit", - "ctrl-shift-e": "pane::RevealInProjectPanel", - "ctrl-f8": "editor::GoToHunk", - "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-shift-;": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "shift-alt-e": "agent::RemoveAllContext" - } - }, - { - "context": "Prompt", - "use_key_equivalents": true, - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", - "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-shift-alt-c": "workspace::CopyRelativePath", - "ctrl-alt-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", - "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", - "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", - "enter": "project_panel::Rename", - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-alt-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", - "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "shift-alt-y": "git::UnstageFile", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "alt-enter": "menu::SecondaryConfirm", - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-up": "git::ForcePush", - "ctrl-g d": "git::Diff", - "ctrl-g backspace": "git::RestoreTrackedFiles", - "ctrl-g shift-backspace": "git::TrashUntrackedFiles", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } - }, - { - "context": "GitDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } - }, - { - "context": "AskPass > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm" - } - }, - { - "context": "CommitEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "git_panel::FocusChanges", - "tab": "git_panel::FocusChanges", - "shift-tab": "git_panel::FocusChanges", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "DebugPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-t": "debugger::ToggleThreadPicker", - "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "VariableList", - "use_key_equivalents": true, - "bindings": { - "left": "variable_list::CollapseSelectedEntry", - "right": "variable_list::ExpandSelectedEntry", - "enter": "variable_list::EditVariable", - "ctrl-c": "variable_list::CopyVariableValue", - "ctrl-alt-c": "variable_list::CopyVariableName", - "delete": "variable_list::RemoveWatch", - "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } - }, - { - "context": "BreakpointList", - "use_key_equivalents": true, - "bindings": { - "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint", - "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } - }, - { - "context": "CollabPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } - }, - { - "context": "CollabPanel", - "use_key_equivalents": true, - "bindings": { - "alt-up": "collab_panel::MoveChannelUp", - "alt-down": "collab_panel::MoveChannelDown" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "use_key_equivalents": true, - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "file_finder::Toggle", - "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-p": "file_finder::SelectPrevious", - "ctrl-j": "pane::SplitDown", - "ctrl-k": "pane::SplitUp", - "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } - }, - { - "context": "TabSwitcher", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-space": "terminal::ShowCharacterPalette", - "copy": "terminal::Copy", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", - "shift-insert": "terminal::Paste", - "ctrl-shift-v": "terminal::Paste", - "ctrl-enter": "assistant::InlineAssist", - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "alt-.": ["terminal::SendText", "\u001b."], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - // Overrides for conflicting keybindings - "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"], - "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"], - "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"], - "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-shift-a": "editor::SelectAll", - "find": "buffer_search::Deploy", - "ctrl-shift-f": "buffer_search::Deploy", - "ctrl-shift-l": "terminal::Clear", - "ctrl-shift-w": "pane::CloseActiveItem", - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-shift-r": "terminal::RerunTask", - "ctrl-alt-r": "terminal::RerunTask", - "alt-t": "terminal::RerunTask" - } - }, - { - "context": "ZedPredictModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "Diagnostics", - "use_key_equivalents": true, - "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } - }, - { - "context": "DebugConsole > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } - }, - { - "context": "RunModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } - }, - { - "context": "MarkdownPreview", - "use_key_equivalents": true, - "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", - "alt-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "ctrl-c": "keymap_editor::CopyAction", - "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } - }, - { - "context": "KeystrokeInput", - "use_key_equivalents": true, - "bindings": { - "enter": "keystroke_input::StartRecording", - "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } - }, - { - "context": "KeybindEditorModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } - }, - { - "context": "KeybindEditorModal > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "ctrl-1": "onboarding::ActivateBasicsPage", - "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } - } -] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd3..be6d34a134 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,13 +428,11 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", - "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", @@ -821,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "workspace::OpenWithSystem", + "s": "project_panel::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 804198090f..c290baf003 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,12 +162,6 @@ // 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, @@ -653,8 +647,6 @@ // "never" "show": "always" }, - // Whether to enable drag-and-drop operations in the project panel. - "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, @@ -1141,6 +1133,11 @@ // 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 @@ -1506,11 +1503,6 @@ // // 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 @@ -1637,9 +1629,6 @@ "allowed": true } }, - "Kotlin": { - "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] - }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], @@ -1653,6 +1642,9 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", + "completions": { + "words": "disabled" + }, "prettier": { "allowed": true } @@ -1666,6 +1658,9 @@ } }, "Plain Text": { + "completions": { + "words": "disabled" + }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 5cead67b6d..a79c550671 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index 0ffbb9f61e..f9f8720729 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": "#fafafaff", + "terminal.ansi.bright_white": "#bfbdb6ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#fcfcfcff", "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": "#fafafaff", + "terminal.ansi.bright_white": "#cccac2ff", "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 f0f0358b76..459825c733 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": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#f9f5d7ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#f2e5bcff", "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 33f6d3c622..23ebbcc67e 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": "#fafafaff", + "terminal.ansi.bright_white": "#dce0e5ff", "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": "#747579ff", + "terminal.ansi.bright_black": "#242529ff", "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": "#ffffffff", + "terminal.ansi.bright_white": "#fafafaff", "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 4ded647a74..61bc50576a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -183,15 +183,16 @@ 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(title.into(), Some(language_registry.clone()), None, cx)), + label: cx.new(|cx| { + Markdown::new( + tool_call.title.into(), + Some(language_registry.clone()), + None, + cx, + ) + }), kind: tool_call.kind, content: tool_call .content @@ -232,30 +233,15 @@ impl ToolCall { if let Some(title) = title { self.label.update(cx, |label, cx| { - if let Some((first_line, _)) = title.split_once("\n") { - label.replace(first_line.to_owned() + "…", cx) - } else { - label.replace(title, cx); - } + label.replace(title, cx); }); } if let Some(content) = content { - 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); + self.content = content + .into_iter() + .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx)) + .collect(); } if let Some(locations) = locations { @@ -512,7 +498,7 @@ impl ContentBlock { "`Image`".into() } - pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), @@ -565,28 +551,6 @@ 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(), @@ -759,8 +723,6 @@ pub struct AcpThread { connection: Rc, session_id: acp::SessionId, token_usage: Option, - prompt_capabilities: acp::PromptCapabilities, - _observe_prompt_capabilities: Task>, } #[derive(Debug)] @@ -775,12 +737,11 @@ pub enum AcpThreadEvent { Stopped, Error, LoadError(LoadError), - PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, @@ -827,20 +788,7 @@ 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(), @@ -852,15 +800,9 @@ 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 } @@ -1045,19 +987,10 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } - 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_title(&mut self, title: SharedString, cx: &mut Context) -> Result<()> { + self.title = title; + cx.emit(AcpThreadEvent::TitleUpdated); + Ok(()) } pub fn update_token_usage(&mut self, usage: Option, cx: &mut Context) { @@ -1360,7 +1293,11 @@ impl AcpThread { }; let git_store = self.project.read(cx).git_store().clone(); - let message_id = if self.connection.truncate(&self.session_id, cx).is_some() { + let message_id = if self + .connection + .session_editor(&self.session_id, cx) + .is_some() + { Some(UserMessageId::new()) } else { None @@ -1398,10 +1335,6 @@ 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| { @@ -1510,7 +1443,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(truncate) = self.connection.truncate(&self.session_id, cx) else { + let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { return Task::ready(Err(anyhow!("not supported"))); }; let Some(message) = self.user_message(&id) else { @@ -1530,7 +1463,8 @@ impl AcpThread { .await?; } - cx.update(|cx| truncate.run(id.clone(), cx))?.await?; + cx.update(|cx| session_editor.truncate(id.clone(), cx))? + .await?; this.update(cx, |this, cx| { if let Some((ix, _)) = this.user_message_mut(&id) { let range = ix..this.entries.len(); @@ -2624,19 +2558,13 @@ 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()); @@ -2670,6 +2598,14 @@ mod tests { } } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); @@ -2683,11 +2619,11 @@ mod tests { .detach(); } - fn truncate( + fn session_editor( &self, session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { + _cx: &mut App, + ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), })) @@ -2702,8 +2638,8 @@ mod tests { _session_id: acp::SessionId, } - impl AgentSessionTruncate for FakeAgentSessionEditor { - fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { + impl AgentSessionEditor for FakeAgentSessionEditor { + fn truncate(&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 af229b7545..2bbd364873 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,29 +38,23 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; + fn prompt_capabilities(&self) -> acp::PromptCapabilities; + fn resume( &self, _session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { None } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - fn truncate( + fn session_editor( &self, _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - fn set_title( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { + _cx: &mut App, + ) -> Option> { None } @@ -85,18 +79,14 @@ impl dyn AgentConnection { } } -pub trait AgentSessionTruncate { - fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; +pub trait AgentSessionEditor { + fn truncate(&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; @@ -327,19 +317,13 @@ 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( @@ -352,6 +336,14 @@ mod test_support { Task::ready(Ok(thread)) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, @@ -432,11 +424,11 @@ mod test_support { } } - fn truncate( + fn session_editor( &self, _session_id: &agent_client_protocol::SessionId, - _cx: &App, - ) -> Option> { + _cx: &mut App, + ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } @@ -447,8 +439,8 @@ mod test_support { struct StubAgentSessionEditor; - impl AgentSessionTruncate for StubAgentSessionEditor { - fn run(&self, _: UserMessageId, _: &mut App) -> Task> { + impl AgentSessionEditor for StubAgentSessionEditor { + fn truncate(&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 0fec6809e0..130bc3ab6b 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,12 +28,10 @@ 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 base_text = old_text.clone().unwrap_or(String::new()).into(); + let buffer = cx.new(|cx| Buffer::local(new_text, cx)); 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) @@ -78,26 +76,32 @@ 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_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_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_diff = cx.new(|cx| { - let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); + let mut diff = BufferDiff::new(&text_snapshot, cx); + let _ = diff.set_base_text( + buffer_snapshot.clone(), + language_registry, + text_snapshot, + cx, + ); 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); + let mut diff = BufferDiff::new(&buffer_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_snapshot, cx); diff }); diff.set_secondary_diff(secondary_diff); + diff }); @@ -115,7 +119,7 @@ impl Diff { diff.update(cx); } }), - new_buffer: buffer, + buffer, diff: buffer_diff, revealed_ranges: Vec::new(), update_diff: Task::ready(Ok(())), @@ -150,9 +154,9 @@ impl Diff { .map(|buffer| buffer.read(cx).text()) .join("\n"); let path = match self { - Diff::Pending(PendingDiff { - new_buffer: buffer, .. - }) => buffer.read(cx).file().map(|file| file.path().as_ref()), + Diff::Pending(PendingDiff { buffer, .. }) => { + buffer.read(cx).file().map(|file| file.path().as_ref()) + } Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), }; format!( @@ -165,33 +169,12 @@ 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, - new_buffer: Entity, + buffer: Entity, diff: Entity, revealed_ranges: Vec>, _subscription: Subscription, @@ -200,7 +183,7 @@ pub struct PendingDiff { impl PendingDiff { pub fn update(&mut self, cx: &mut Context) { - let buffer = self.new_buffer.clone(); + let buffer = self.buffer.clone(); let buffer_diff = self.diff.clone(); let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { @@ -238,10 +221,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.new_buffer.read(cx).language_registry(); + let language_registry = self.buffer.read(cx).language_registry(); let path = self - .new_buffer + .buffer .read(cx) .file() .map(|file| file.path().as_ref()) @@ -250,12 +233,12 @@ impl PendingDiff { // Replace the buffer in the multibuffer with the snapshot let buffer = cx.new(|cx| { - let language = self.new_buffer.read(cx).language().cloned(); + let language = self.buffer.read(cx).language().cloned(); let buffer = TextBuffer::new_normalized( 0, cx.entity_id().as_non_zero_u64().into(), - self.new_buffer.read(cx).line_ending(), - self.new_buffer.read(cx).as_rope().clone(), + self.buffer.read(cx).line_ending(), + self.buffer.read(cx).as_rope().clone(), ); let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); buffer.set_language(language, cx); @@ -291,9 +274,7 @@ impl PendingDiff { FinalizedDiff { path, - base_text: self.base_text.clone(), multibuffer: self.multibuffer.clone(), - new_buffer: self.new_buffer.clone(), _update_diff: update_diff, } } @@ -302,8 +283,8 @@ impl PendingDiff { let ranges = self.excerpt_ranges(cx); self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&self.new_buffer, cx), - self.new_buffer.clone(), + PathKey::for_buffer(&self.buffer, cx), + self.buffer.clone(), ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, @@ -315,7 +296,7 @@ impl PendingDiff { } fn excerpt_ranges(&self, cx: &App) -> Vec> { - let buffer = self.new_buffer.read(cx); + let buffer = self.buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) @@ -349,8 +330,6 @@ impl PendingDiff { pub struct FinalizedDiff { path: PathBuf, - base_text: Arc, - new_buffer: Entity, multibuffer: Entity, _update_diff: Task>, } @@ -404,21 +383,3 @@ 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 6fa0887e22..a1e713cffa 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::RangeInclusive, + ops::Range, path::{Path, PathBuf}, str::FromStr, }; @@ -17,14 +17,13 @@ pub enum MentionUri { File { abs_path: PathBuf, }, - PastedImage, Directory { abs_path: PathBuf, }, Symbol { - abs_path: PathBuf, + path: PathBuf, name: String, - line_range: RangeInclusive, + line_range: Range, }, Thread { id: acp::SessionId, @@ -39,9 +38,8 @@ pub enum MentionUri { name: String, }, Selection { - #[serde(default, skip_serializing_if = "Option::is_none")] - abs_path: Option, - line_range: RangeInclusive, + path: PathBuf, + line_range: Range, }, Fetch { url: Url, @@ -50,44 +48,36 @@ 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 line_range = parse_line_range(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")?; if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - abs_path: path, + path, line_range, }) } else { - Ok(Self::Selection { - abs_path: Some(path), - line_range, - }) + Ok(Self::Selection { path, line_range }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path }) @@ -115,17 +105,6 @@ 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); } @@ -142,16 +121,13 @@ 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 { - abs_path: path, - line_range, - .. - } => selection_name(path.as_deref(), line_range), + path, line_range, .. + } => selection_name(path, line_range), MentionUri::Fetch { url } => url.to_string(), } } @@ -161,7 +137,6 @@ 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(), @@ -182,40 +157,29 @@ 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 { - abs_path, + path, name, line_range, } => { - let mut url = - Url::from_file_path(abs_path).expect("mention path should be absolute"); + let mut url = Url::from_file_path(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 { - 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 - }; + MentionUri::Selection { path, line_range } => { + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start() + 1, - line_range.end() + 1 + line_range.start + 1, + line_range.end + 1 ))); url } @@ -227,10 +191,7 @@ impl MentionUri { } MentionUri::TextThread { path, name } => { let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!( - "/agent/text-thread/{}", - path.to_string_lossy().trim_start_matches('/') - )); + url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); url.query_pairs_mut().append_pair("name", name); url } @@ -276,14 +237,12 @@ fn single_query_param(url: &Url, name: &'static str) -> Result> { } } -pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { +pub fn selection_name(path: &Path, line_range: &Range) -> String { format!( "{} ({}:{})", - path.and_then(|path| path.file_name()) - .unwrap_or("Untitled".as_ref()) - .display(), - *line_range.start() + 1, - *line_range.end() + 1 + path.file_name().unwrap_or_default().display(), + line_range.start + 1, + line_range.end + 1 ) } @@ -343,14 +302,14 @@ mod tests { let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { - abs_path: 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"), } @@ -362,39 +321,16 @@ mod tests { let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { - 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); + 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); } _ => 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 deleted file mode 100644 index 7a6d8c21a0..0000000000 --- a/crates/acp_tools/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[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 deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/acp_tools/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../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 deleted file mode 100644 index e20a040e9d..0000000000 --- a/crates/acp_tools/src/acp_tools.rs +++ /dev/null @@ -1,494 +0,0 @@ -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!(dev, [OpenAcpLogs]); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &OpenAcpLogs, 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/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cba2457566..45e551dbdf 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,19 +893,8 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { 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/src/agent.rs b/crates/agent2/src/agent.rs index 6fa36d33d5..d5bc0fea63 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -2,7 +2,7 @@ use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated}; +use crate::{HistoryStore, 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::debug!("Creating new NativeAgent"); + log::info!("Creating new NativeAgent"); let project_context = cx .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? @@ -240,23 +240,19 @@ impl NativeAgent { let title = thread.title(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); - let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); - let acp_thread = cx.new(|cx| { + 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, cx) @@ -445,26 +441,6 @@ 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, @@ -741,6 +717,10 @@ impl NativeAgentConnection { thread.update_tool_call(update, 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) @@ -759,7 +739,7 @@ impl NativeAgentConnection { } } - log::debug!("Response stream completed"); + log::info!("Response stream completed"); anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) @@ -784,7 +764,7 @@ impl AgentModelSelector for NativeAgentConnection { model_id: acp_thread::AgentModelId, cx: &mut App, ) -> Task> { - log::debug!("Setting model for session {}: {}", session_id, model_id); + log::info!("Setting model for session {}: {}", session_id, model_id); let Some(thread) = self .0 .read(cx) @@ -855,11 +835,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Task>> { let agent = self.0.clone(); - log::debug!("Creating new thread for project at: {:?}", cwd); + log::info!("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, @@ -875,16 +856,20 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - Ok(cx.new(|cx| { + + let thread = 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)) @@ -920,7 +905,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .into_iter() .map(Into::into) .collect::>(); - log::debug!("Converted prompt to message: {} chars", content.len()); + log::info!("Converted prompt to message: {} chars", content.len()); log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); @@ -928,10 +913,18 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn resume( &self, session_id: &acp::SessionId, - _cx: &App, + _cx: &mut App, ) -> Option> { Some(Rc::new(NativeAgentSessionResume { connection: self.clone(), @@ -948,12 +941,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }); } - fn truncate( + fn session_editor( &self, session_id: &agent_client_protocol::SessionId, - cx: &App, - ) -> Option> { - self.0.read_with(cx, |agent, _cx| { + cx: &mut App, + ) -> Option> { + self.0.update(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { thread: session.thread.clone(), @@ -963,17 +956,6 @@ 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) } @@ -1009,8 +991,8 @@ struct NativeAgentSessionEditor { acp_thread: WeakEntity, } -impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { - fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { +impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { + fn truncate(&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()) @@ -1042,22 +1024,6 @@ 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; @@ -1357,8 +1323,6 @@ mod tests { ) }); - cx.run_until_parked(); - // Drop the ACP thread, which should cause the session to be dropped as well. cx.update(|_| { drop(thread); @@ -1401,9 +1365,10 @@ mod tests { history: &Entity, cx: &mut TestAppContext, ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, _| { + history.read_with(cx, |history, cx| { history - .entries() + .entries(cx) + .iter() .map(|e| (e.id(), e.title().to_string())) .collect::>() }) diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index e7d31c0c7a..1b88955a24 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -266,19 +266,8 @@ impl ThreadsDatabase { } pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { 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)?; diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index c656456e01..78d83cc1d0 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -86,7 +86,6 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, - entries: Vec, context_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, @@ -98,7 +97,7 @@ impl HistoryStore { context_store: Entity, cx: &mut Context, ) -> Self { - let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; + let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await; @@ -117,7 +116,6 @@ impl HistoryStore { context_store, recently_opened_entries: VecDeque::default(), threads: Vec::default(), - entries: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), } @@ -183,18 +181,20 @@ impl HistoryStore { } } this.threads = threads; - this.update_entries(cx); + cx.notify(); }) }) .detach_and_log_err(cx); } - fn update_entries(&mut self, cx: &mut Context) { + pub fn entries(&self, cx: &App) -> Vec { + let mut history_entries = Vec::new(); + #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return; + return history_entries; } - let mut history_entries = Vec::new(); + history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend( self.context_store @@ -205,12 +205,17 @@ impl HistoryStore { ); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); - self.entries = history_entries; - cx.notify() + history_entries } - pub fn is_empty(&self, _cx: &App) -> bool { - self.entries.is_empty() + pub fn is_empty(&self, cx: &App) -> bool { + self.threads.is_empty() + && self + .context_store + .read(cx) + .unordered_contexts() + .next() + .is_none() } pub fn recently_opened_entries(&self, cx: &App) -> Vec { @@ -351,7 +356,7 @@ impl HistoryStore { self.save_recently_opened_entries(cx); } - pub fn entries(&self) -> impl Iterator { - self.entries.iter().cloned() + pub fn recent_entries(&self, limit: usize, cx: &mut Context) -> Vec { + self.entries(cx).into_iter().take(limit).collect() } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 9ff98ccd18..ac5aa95c04 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, SharedString, Task}; +use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; @@ -22,20 +22,16 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { - fn telemetry_id(&self) -> &'static str { - "zed" + fn name(&self) -> &'static str { + "Native Agent" } - fn name(&self) -> SharedString { - "Zed Agent".into() + fn empty_state_headline(&self) -> &'static str { + "Welcome to the Agent Panel" } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "".into() + fn empty_state_message(&self) -> &'static str { + "" } fn logo(&self) -> ui::IconName { @@ -48,7 +44,7 @@ impl AgentServer for NativeAgentServer { project: &Entity, cx: &mut App, ) -> Task>> { - log::debug!( + log::info!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); @@ -67,7 +63,7 @@ impl AgentServer for NativeAgentServer { // Create the connection wrapper let connection = NativeAgentConnection(agent); - log::debug!("NativeAgentServer connection established successfully"); + log::info!("NativeAgentServer connection established successfully"); Ok(Rc::new(connection) as Rc) }) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index fbeee46a48..3bd1be497e 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,40 +1,30 @@ 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::{self, UnboundedReceiver}, - oneshot, - }, -}; +use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat, - LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, + LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, + fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; -use project::{ - Project, context_server_store::ContextServerStore, project_settings::ProjectSettings, -}; +use project::Project; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; -use settings::{Settings, SettingsStore}; +use settings::SettingsStore; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -42,22 +32,17 @@ mod test_tools; use test_tools::*; #[gpui::test] +#[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; let events = thread .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) - .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; + .unwrap() + .collect() + .await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -72,10 +57,9 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows +#[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; let events = thread .update(cx, |thread, cx| { @@ -90,18 +74,9 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) - .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; + .unwrap() + .collect() + .await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -235,7 +210,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, @@ -248,7 +223,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()), @@ -296,7 +271,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] +#[ignore = "can't run on CI yet"] async fn test_basic_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -318,7 +293,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(&EchoTool::name()); + thread.remove_tool(&AgentTool::name(&EchoTool)); thread.add_tool(DelayTool); thread.send( UserMessageId::new(), @@ -356,7 +331,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] +#[ignore = "can't run on CI yet"] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -422,7 +397,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, @@ -431,7 +406,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, @@ -462,17 +437,17 @@ 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: Some("Permission to run tool denied by user".into()) + output: None }) ] ); @@ -481,7 +456,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, @@ -503,7 +478,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()) @@ -515,7 +490,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, @@ -530,7 +505,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()) @@ -582,7 +557,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, @@ -595,7 +570,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()), @@ -675,6 +650,15 @@ 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] @@ -692,14 +676,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()), @@ -810,7 +794,7 @@ async fn next_tool_call_authorization( } #[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] +#[ignore = "can't run on CI yet"] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -876,14 +860,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, } } } @@ -912,7 +896,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. @@ -931,339 +915,11 @@ 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] -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)] +#[ignore = "can't run on CI yet"] async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -1348,7 +1004,6 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1687,7 +1342,6 @@ 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(); @@ -1715,7 +1369,6 @@ 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. @@ -1733,81 +1386,6 @@ 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); @@ -1959,7 +1537,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, @@ -2100,7 +1678,6 @@ 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)), @@ -2110,9 +1687,8 @@ 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("there!"); + fake_model.send_last_completion_stream_text_chunk("Hey!"); fake_model.end_last_completion_stream(); - cx.run_until_parked(); let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { @@ -2140,94 +1716,12 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { ## Assistant - Hey, - - [resume] - - ## Assistant - - there! + Hey! "} ) }); } -#[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; @@ -2298,12 +1792,12 @@ struct ThreadTest { model: Arc, thread: Entity, project_context: Entity, - context_server_store: Entity, fs: Arc, } enum TestModel { Sonnet4, + Sonnet4Thinking, Fake, } @@ -2311,6 +1805,7 @@ 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!(), } } @@ -2332,12 +1827,11 @@ 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, - ThinkingTool::name(): true, + EchoTool.name(): true, + DelayTool.name(): true, + WordListTool.name(): true, + ToolRequiringPermission.name(): true, + InfiniteTool.name(): true, } } } @@ -2394,14 +1888,15 @@ 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(context_server_store.clone(), cx)); + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let action_log = cx.new(|_| ActionLog::new(project.clone())); let thread = cx.new(|cx| { Thread::new( project, project_context.clone(), context_server_registry, + action_log, templates, Some(model.clone()), cx, @@ -2411,7 +1906,6 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { model, thread, project_context, - context_server_store, fs, } } @@ -2446,89 +1940,3 @@ 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 27be7b6ac3..cbff44cedf 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() -> &'static str { - "echo" + fn name(&self) -> SharedString { + "echo".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } @@ -51,8 +51,8 @@ impl AgentTool for DelayTool { type Input = DelayToolInput; type Output = String; - fn name() -> &'static str { - "delay" + fn name(&self) -> SharedString { + "delay".into() } fn initial_title(&self, input: Result) -> SharedString { @@ -63,7 +63,7 @@ impl AgentTool for DelayTool { } } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } @@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission { type Input = ToolRequiringPermissionInput; type Output = String; - fn name() -> &'static str { - "tool_requiring_permission" + fn name(&self) -> SharedString { + "tool_requiring_permission".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } @@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool { type Input = InfiniteToolInput; type Output = String; - fn name() -> &'static str { - "infinite" + fn name(&self) -> SharedString { + "infinite".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } @@ -178,11 +178,11 @@ impl AgentTool for WordListTool { type Input = WordListInput; type Output = String; - fn name() -> &'static str { - "word_list" + fn name(&self) -> SharedString { + "word_list".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 97ea1caf1d..6f560cd390 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -9,15 +9,15 @@ use action_log::ActionLog; use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; use agent_settings::{ - AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, - SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, + AgentProfileId, 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 client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; -use collections::{HashMap, HashSet, IndexMap}; +use collections::{HashMap, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -45,19 +45,17 @@ 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 util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; +use std::{fmt::Write, ops::Range}; +use util::{ResultExt, 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. /// @@ -123,7 +121,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resume]\n".into(), + Message::Resume => "[resumed after tool use limit was reached]".into(), } } @@ -188,7 +186,6 @@ 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 = @@ -197,7 +194,6 @@ 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(); @@ -214,7 +210,7 @@ impl UserMessage { match uri { MentionUri::File { abs_path } => { write!( - &mut file_context, + &mut symbol_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(abs_path, None), @@ -223,19 +219,17 @@ 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 { - abs_path: path, - line_range, - .. + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. } => { write!( - &mut symbol_context, + &mut rules_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(path, Some(line_range)), @@ -244,24 +238,6 @@ 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(); } @@ -314,13 +290,6 @@ 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 @@ -356,7 +325,7 @@ impl UserMessage { } } -fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { +fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { let mut result = String::new(); if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { @@ -366,10 +335,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> 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); } } @@ -448,33 +417,24 @@ impl AgentMessage { cache: false, }; for chunk in &self.content { - match chunk { + let chunk = match chunk { AgentMessageContent::Text(text) => { - assistant_message - .content - .push(language_model::MessageContent::Text(text.clone())); + language_model::MessageContent::Text(text.clone()) } AgentMessageContent::Thinking { text, signature } => { - assistant_message - .content - .push(language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - AgentMessageContent::RedactedThinking(value) => { - assistant_message.content.push( - language_model::MessageContent::RedactedThinking(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())); + language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), } } + AgentMessageContent::RedactedThinking(value) => { + language_model::MessageContent::RedactedThinking(value.clone()) + } + AgentMessageContent::ToolUse(value) => { + language_model::MessageContent::ToolUse(value.clone()) + } }; + assistant_message.content.push(chunk); } let mut user_message = LanguageModelRequestMessage { @@ -527,6 +487,7 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -553,7 +514,6 @@ pub struct Thread { prompt_id: PromptId, updated_at: DateTime, title: Option, - pending_title_generation: Option>, summary: Option, messages: Vec, completion_mode: CompletionMode, @@ -575,40 +535,26 @@ 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, @@ -630,8 +576,6 @@ impl Thread { templates, model, summarization_model: None, - prompt_capabilities_tx, - prompt_capabilities_rx, project, action_log, } @@ -682,20 +626,7 @@ impl Thread { stream: &ThreadEventStream, cx: &mut Context, ) { - 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 { + let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { stream .0 .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { @@ -732,17 +663,7 @@ impl Thread { stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { - status: Some( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ), + status: Some(acp::ToolCallStatus::Completed), raw_output: output, ..Default::default() }, @@ -775,8 +696,6 @@ impl Thread { .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, @@ -786,7 +705,6 @@ 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(), @@ -806,8 +724,6 @@ impl Thread { project, action_log, updated_at: db_thread.updated_at, - prompt_capabilities_tx, - prompt_capabilities_rx, } } @@ -975,12 +891,10 @@ impl Thread { 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() } @@ -1043,11 +957,11 @@ impl Thread { )); self.add_tool(TerminalTool::new(self.project.clone(), cx)); self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); + self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. } - pub fn add_tool(&mut self, tool: T) { - self.tools.insert(T::name().into(), tool.erase()); + pub fn add_tool(&mut self, tool: impl AgentTool) { + self.tools.insert(tool.name(), tool.erase()); } pub fn remove_tool(&mut self, name: &str) -> bool { @@ -1116,10 +1030,15 @@ 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::debug!("Total messages in thread: {}", self.messages.len()); + log::info!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1137,7 +1056,7 @@ impl Thread { { let model = self.model().context("No language model configured")?; - log::info!("Thread::send called with model: {}", model.name().0); + log::info!("Thread::send called with model: {:?}", model.name()); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -1147,7 +1066,7 @@ impl Thread { .push(Message::User(UserMessage { id, content })); cx.notify(); - log::debug!("Total messages in thread: {}", self.messages.len()); + log::info!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1158,10 +1077,6 @@ 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); @@ -1169,16 +1084,49 @@ 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::debug!("Starting agent turn execution"); + log::info!("Starting agent turn execution"); + let mut update_title = None; + let turn_result: Result<()> = async { + let mut intent = CompletionIntent::UserPrompt; + loop { + Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; - let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; + let mut end_turn = true; + this.update(cx, |this, cx| { + // Generate title if needed. + if this.title.is_none() && update_title.is_none() { + update_title = Some(this.update_title(&event_stream, cx)); + } + + // End the turn if the model didn't use tools. + let message = this.pending_message.as_ref(); + end_turn = + message.map_or(true, |message| message.tool_results.is_empty()); + this.flush_pending_message(cx); + })?; + + if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + log::info!("Tool use limit reached, completing turn"); + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + } + } + } + .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); + if let Some(update_title) = update_title { + update_title.await.context("update title failed").log_err(); + } + match turn_result { Ok(()) => { - log::debug!("Turn execution completed"); + log::info!("Turn execution completed"); event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { @@ -1204,18 +1152,20 @@ impl Thread { Ok(events_rx) } - async fn run_turn_internal( + async fn stream_completion( this: &WeakEntity, - model: Arc, + model: &Arc, + completion_intent: CompletionIntent, event_stream: &ThreadEventStream, cx: &mut AsyncApp, ) -> Result<()> { - let mut attempt = 0; - let mut intent = CompletionIntent::UserPrompt; - loop { - let request = - this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; + log::debug!("Stream completion started successfully"); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; + let mut attempt = None; + 'retry: loop { telemetry::event!( "Agent Thread Completion", thread_id = this.read_with(cx, |this, _| this.id.to_string())?, @@ -1225,31 +1175,75 @@ impl Thread { attempt ); - log::debug!("Calling model.stream_completion, attempt {}", attempt); + log::info!( + "Calling model.stream_completion, attempt {}", + attempt.unwrap_or(0) + ); let mut events = model - .stream_completion(request, cx) + .stream_completion(request.clone(), 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(event) => { + log::trace!("Received completion event: {:?}", event); tool_results.extend(this.update(cx, |this, cx| { - this.handle_completion_event(event, event_stream, cx) + this.handle_streamed_completion_event(event, event_stream, cx) })??); } - Err(err) => { - error = Some(err); - break; + Err(error) => { + let completion_mode = + this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(anyhow!(error))?; + } + + 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, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + 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:?}"); + + 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; } } } - let end_turn = tool_results.is_empty(); while let Some(tool_result) = tool_results.next().await { - log::debug!("Tool finished {:?}", tool_result); + log::info!("Tool finished {:?}", tool_result); event_stream.update_tool_call_fields( &tool_result.tool_use_id, @@ -1270,83 +1264,31 @@ impl Thread { })?; } - 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; - } + return Ok(()); } } - fn handle_completion_error( - &mut self, - error: LanguageModelCompletionError, - attempt: u8, - ) -> Result { - if self.completion_mode == CompletionMode::Normal { - return Err(anyhow!(error)); + 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(), } - - 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)); + .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 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 will /// send back to the model when it resolves. - fn handle_completion_event( + fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, @@ -1477,7 +1419,7 @@ impl Thread { ) -> Option> { cx.notify(); - let tool = self.tool(tool_use.name.as_ref()); + let tool = self.tools.get(tool_use.name.as_ref()).cloned(); let mut title = SharedString::from(&tool_use.name); let mut kind = acp::ToolKind::Other; if let Some(tool) = tool.as_ref() { @@ -1541,7 +1483,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::debug!("Running tool {}", tool_use.name); + log::info!("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 @@ -1567,7 +1509,7 @@ impl Thread { tool_name: tool_use.name, is_error: true, content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), + output: None, }, } })) @@ -1653,7 +1595,7 @@ impl Thread { summary.extend(lines.next()); } - log::debug!("Setting summary: {}", summary); + log::info!("Setting summary: {}", summary); let summary = SharedString::from(summary); this.update(cx, |this, cx| { @@ -1665,15 +1607,19 @@ impl Thread { }) } - fn generate_title(&mut self, cx: &mut Context) { - let Some(model) = self.summarization_model.clone() else { - return; - }; - - log::debug!( + fn update_title( + &mut self, + event_stream: &ThreadEventStream, + cx: &mut Context, + ) -> Task> { + log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); + let Some(model) = self.summarization_model.clone() else { + return Task::ready(Ok(())); + }; + let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), @@ -1689,51 +1635,42 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - self.pending_title_generation = Some(cx.spawn(async move |this, cx| { + cx.spawn(async move |this, cx| { let mut title = String::new(); - - 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; + 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; } - anyhow::Ok(()) - }; - - if generate.await.context("failed to generate title").is_ok() { - _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); } - _ = 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(); - } + 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(); + }) + }) } fn last_user_message(&self) -> Option<&UserMessage> { @@ -1756,10 +1693,6 @@ impl Thread { return; }; - if message.content.is_empty() { - return; - } - for content in &message.content { let AgentMessageContent::ToolUse(tool_use) = content else { continue; @@ -1788,32 +1721,34 @@ impl Thread { pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, - cx: &App, + cx: &mut 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::debug!("Request will include {} messages", messages.len()); - log::debug!("Request includes {} tools", tools.len()); + 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()); let request = LanguageModelRequest { thread_id: Some(self.id.to_string()), @@ -1832,76 +1767,37 @@ impl Thread { Ok(request) } - 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() - } - } + fn tools<'a>(&'a self, cx: &'a App) -> Result>> { + let model = self.model().context("No language model configured")?; - let mut tools = self + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("profile not found")?; + let provider_id = model.provider_id(); + + Ok(self .tools .iter() + .filter(move |(_, tool)| tool.supported_provider(&provider_id)) .filter_map(|(tool_name, tool)| { - if tool.supported_provider(&model.provider_id()) - && profile.is_tool_enabled(tool_name) - { - Some((truncate(tool_name), tool.clone())) + if profile.is_tool_enabled(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() + .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 + } + }) + }, + ))) } fn build_request_messages(&self, cx: &App) -> Vec { @@ -1909,31 +1805,23 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); - - 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, - }]; + let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { messages.extend(message.to_request()); } - if let Some(last_message) = messages.last_mut() { - last_message.cache = true; - } - if let Some(message) = self.pending_message.as_ref() { messages.extend(message.to_request()); } + if let Some(last_user_message) = messages + .iter_mut() + .rev() + .find(|message| message.role == Role::User) + { + last_user_message.cache = true; + } + messages } @@ -2074,8 +1962,6 @@ 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 { @@ -2089,10 +1975,6 @@ pub struct TokenUsageUpdated(pub Option); impl EventEmitter for Thread {} -pub struct TitleUpdated; - -impl EventEmitter for Thread {} - pub trait AgentTool where Self: 'static + Sized, @@ -2100,7 +1982,7 @@ where type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; type Output: for<'de> Deserialize<'de> + Serialize + Into; - fn name() -> &'static str; + fn name(&self) -> SharedString; fn description(&self) -> SharedString { let schema = schemars::schema_for!(Self::Input); @@ -2112,7 +1994,7 @@ where ) } - fn kind() -> acp::ToolKind; + fn kind(&self) -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. fn initial_title(&self, input: Result) -> SharedString; @@ -2188,7 +2070,7 @@ where T: AgentTool, { fn name(&self) -> SharedString { - T::name().into() + self.0.name() } fn description(&self) -> SharedString { @@ -2196,7 +2078,7 @@ where } fn kind(&self) -> agent_client_protocol::ToolKind { - T::kind() + self.0.kind() } fn initial_title(&self, input: serde_json::Value) -> SharedString { @@ -2250,6 +2132,12 @@ 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()))) @@ -2469,30 +2357,6 @@ impl ToolCallEventStreamReceiver { } } - pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - update, - )))) = event - { - update.fields - } else { - panic!("Expected update fields but got: {:?}", event); - } - } - - pub async fn expect_diff(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( - update, - )))) = event - { - update.diff - } else { - panic!("Expected diff but got: {:?}", event); - } - } - pub async fn expect_terminal(&mut self) -> Entity { let event = self.0.next().await; if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index bcca7eecd1..d1f2b3b1c7 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -16,29 +16,6 @@ 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::*; @@ -56,5 +33,3 @@ 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/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index 819a6ff209..4b40a9842f 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -1,7 +1,7 @@ use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol::ToolKind; use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, Task}; +use gpui::{App, AppContext, Entity, SharedString, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -50,11 +50,11 @@ impl AgentTool for CopyPathTool { type Input = CopyPathToolInput; type Output = String; - fn name() -> &'static str { - "copy_path" + fn name(&self) -> SharedString { + "copy_path".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index 652363d5fa..7720eb3595 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -41,11 +41,11 @@ impl AgentTool for CreateDirectoryTool { type Input = CreateDirectoryToolInput; type Output = String; - fn name() -> &'static str { - "create_directory" + fn name(&self) -> SharedString { + "create_directory".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index 0f9641127f..c281f1b5b6 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -44,11 +44,11 @@ impl AgentTool for DeletePathTool { type Input = DeletePathToolInput; type Output = String; - fn name() -> &'static str { - "delete_path" + fn name(&self) -> SharedString { + "delete_path".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Delete } diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index 558bb918ce..6ba8b7b377 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() -> &'static str { - "diagnostics" + fn name(&self) -> SharedString { + "diagnostics".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> 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 f86bfd25f7..f89cace9a8 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -186,11 +186,11 @@ impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; - fn name() -> &'static str { - "edit_file" + fn name(&self) -> SharedString { + "edit_file".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Edit } @@ -273,13 +273,6 @@ impl AgentTool for EditFileTool { let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; event_stream.update_diff(diff.clone()); - let _finalize_diff = util::defer({ - let diff = diff.downgrade(); - let mut cx = cx.clone(); - move || { - diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); - } - }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -396,6 +389,8 @@ impl AgentTool for EditFileTool { }) .await; + diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); + let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( @@ -522,6 +517,7 @@ 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}; @@ -539,6 +535,7 @@ 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()); @@ -547,6 +544,7 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, + action_log, Templates::new(), Some(model), cx, @@ -737,6 +735,7 @@ 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()); @@ -745,6 +744,7 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, + action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -801,9 +801,7 @@ mod tests { "Code should be formatted when format_on_save is enabled" ); - let stale_buffer_count = thread - .read_with(cx, |thread, _cx| thread.action_log.clone()) - .read_with(cx, |log, cx| log.stale_buffers(cx).count()); + let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); assert_eq!( stale_buffer_count, 0, @@ -881,12 +879,14 @@ 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, @@ -1008,12 +1008,14 @@ 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, @@ -1144,12 +1146,14 @@ 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, @@ -1250,6 +1254,7 @@ 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()); @@ -1258,6 +1263,7 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), + action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1330,6 +1336,7 @@ 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()); @@ -1338,6 +1345,7 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), + action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1413,6 +1421,7 @@ 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()); @@ -1421,6 +1430,7 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), + action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1493,6 +1503,7 @@ 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()); @@ -1501,6 +1512,7 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, + action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1550,100 +1562,6 @@ mod tests { ); } - #[gpui::test] - async fn test_diff_finalization(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({"main.rs": ""})).await; - - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - let languages = 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 model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry.clone(), - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // Ensure the diff is finalized after the edit completes. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - cx.run_until_parked(); - model.end_last_completion_stream(); - edit.await.unwrap(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - - // Ensure the diff is finalized if an error occurs while editing. - { - model.forbid_requests(); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - edit.await.unwrap_err(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - model.allow_requests(); - } - - // Ensure the diff is finalized if the tool call gets dropped. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - drop(edit); - cx.run_until_parked(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index dd97271a79..ae26c5fe19 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() -> &'static str { - "fetch" + fn name(&self) -> SharedString { + "fetch".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Fetch } @@ -136,17 +136,12 @@ 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 { - authorize.await?; - Self::build_message(http_client, &input.url).await - } + async move { 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 384bd56e77..9e11ca6a37 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -85,11 +85,11 @@ impl AgentTool for FindPathTool { type Input = FindPathToolInput; type Output = FindPathToolOutput; - fn name() -> &'static str { - "find_path" + fn name(&self) -> SharedString { + "find_path".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Search } @@ -165,17 +165,16 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task &'static str { - "grep" + fn name(&self) -> SharedString { + "grep".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Search } diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index e6fa8d7431..31575a92e4 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -51,11 +51,11 @@ impl AgentTool for ListDirectoryTool { type Input = ListDirectoryToolInput; type Output = String; - fn name() -> &'static str { - "list_directory" + fn name(&self) -> SharedString { + "list_directory".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index d9fb60651b..2a173a4404 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -52,11 +52,11 @@ impl AgentTool for MovePathTool { type Input = MovePathToolInput; type Output = String; - fn name() -> &'static str { - "move_path" + fn name(&self) -> SharedString { + "move_path".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index 9467e7db68..a72ede26fe 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() -> &'static str { - "now" + fn name(&self) -> SharedString { + "now".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index df7b04c787..c20369c2d8 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -37,11 +37,11 @@ impl AgentTool for OpenTool { type Input = OpenToolInput; type Output = String; - fn name() -> &'static str { - "open" + fn name(&self) -> SharedString { + "open".into() } - fn kind() -> ToolKind { + fn kind(&self) -> ToolKind { ToolKind::Execute } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index e771c26eca..11a57506fb 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -10,8 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::{path::Path, sync::Arc}; -use util::markdown::MarkdownCodeBlock; +use std::sync::Arc; use crate::{AgentTool, ToolCallEventStream}; @@ -60,21 +59,36 @@ impl AgentTool for ReadFileTool { type Input = ReadFileToolInput; type Output = LanguageModelToolResultContent; - fn name() -> &'static str { - "read_file" + fn name(&self) -> SharedString { + "read_file".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Read } fn initial_title(&self, input: Result) -> SharedString { - 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() + 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() + } } fn run( @@ -244,19 +258,6 @@ 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 f41b909d0b..3d4faf2e03 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() -> &'static str { - "terminal" + fn name(&self) -> SharedString { + "terminal".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Execute } diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index 61fb9eb0d6..c5e9451162 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -21,11 +21,11 @@ impl AgentTool for ThinkingTool { type Input = ThinkingToolInput; type Output = String; - fn name() -> &'static str { - "thinking" + fn name(&self) -> SharedString { + "thinking".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> 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 d7a34bec29..ffcd4ad3be 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool { type Input = WebSearchToolInput; type Output = WebSearchToolOutput; - fn name() -> &'static str { - "web_search" + fn name(&self) -> SharedString { + "web_search".into() } - fn kind() -> acp::ToolKind { + fn kind(&self) -> acp::ToolKind { acp::ToolKind::Fetch } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 9f90f3a78a..60dd796463 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -17,11 +17,11 @@ 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 diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4e897374a..1cfb1fcabf 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -1,414 +1,34 @@ +use std::{path::Path, rc::Rc}; + use crate::AgentServerCommand; use acp_thread::AgentConnection; -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 anyhow::Result; +use gpui::AsyncApp; use thiserror::Error; -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; - -use acp_thread::{AcpThread, AuthRequired, LoadError}; +mod v0; +mod v1; #[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: SharedString, + server_name: &'static str, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?; - Ok(Rc::new(conn) as _) -} + let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; -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()); + 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) } - - 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(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - let command = configuration.command(); - Some(acp::McpServer { - name: id.0.to_string(), - command: command.path.clone(), - args: command.args.clone(), - env: if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable { - name: name.clone(), - value: value.clone(), - }) - .collect() - } else { - vec![] - }, - }) - }) - .collect(); - - cx.spawn(async move |cx| { - let response = conn - .new_session(acp::NewSessionRequest { mcp_servers, 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(()) + Err(err) => Err(err), } } diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs new file mode 100644 index 0000000000..be96048929 --- /dev/null +++ b/crates/agent_servers/src/acp/v0.rs @@ -0,0 +1,524 @@ +// Translates old acp agents into the new schema +use action_log::ActionLog; +use agent_client_protocol as acp; +use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use project::Project; +use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; +use ui::App; +use util::ResultExt as _; + +use crate::AgentServerCommand; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; + +#[derive(Clone)] +struct OldAcpClientDelegate { + thread: Rc>>, + cx: AsyncApp, + next_tool_call_id: Rc>, + // sent_buffer_versions: HashMap, HashMap>, +} + +impl OldAcpClientDelegate { + fn new(thread: Rc>>, cx: AsyncApp) -> Self { + Self { + thread, + cx, + next_tool_call_id: Rc::new(RefCell::new(0)), + } + } +} + +impl acp_old::Client for OldAcpClientDelegate { + async fn stream_assistant_message_chunk( + &self, + params: acp_old::StreamAssistantMessageChunkParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| match params.chunk { + acp_old::AssistantMessageChunk::Text { text } => { + thread.push_assistant_content_block(text.into(), false, cx) + } + acp_old::AssistantMessageChunk::Thought { thought } => { + thread.push_assistant_content_block(thought.into(), true, cx) + } + }) + .log_err(); + })?; + + Ok(()) + } + + async fn request_tool_call_confirmation( + &self, + request: acp_old::RequestToolCallConfirmationParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + let tool_call = into_new_tool_call( + acp::ToolCallId(old_acp_id.to_string().into()), + request.tool_call, + ); + + let mut options = match request.confirmation { + acp_old::ToolCallConfirmation::Edit { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow Edits".to_string(), + )], + acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", root_command), + )], + acp_old::ToolCallConfirmation::Mcp { + server_name, + tool_name, + .. + } => vec![ + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", server_name), + ), + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", tool_name), + ), + ], + acp_old::ToolCallConfirmation::Fetch { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + acp_old::ToolCallConfirmation::Other { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + }; + + options.extend([ + ( + acp_old::ToolCallConfirmationOutcome::Allow, + acp::PermissionOptionKind::AllowOnce, + "Allow".to_string(), + ), + ( + acp_old::ToolCallConfirmationOutcome::Reject, + acp::PermissionOptionKind::RejectOnce, + "Reject".to_string(), + ), + ]); + + let mut outcomes = Vec::with_capacity(options.len()); + let mut acp_options = Vec::with_capacity(options.len()); + + for (index, (outcome, kind, label)) in options.into_iter().enumerate() { + outcomes.push(outcome); + acp_options.push(acp::PermissionOption { + id: acp::PermissionOptionId(index.to_string().into()), + name: label, + kind, + }) + } + + let response = cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) + }) + })?? + .context("Failed to update thread")? + .await; + + let outcome = match response { + Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], + Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, + }; + + Ok(acp_old::RequestToolCallConfirmationResponse { + id: acp_old::ToolCallId(old_acp_id), + outcome, + }) + } + + async fn push_tool_call( + &self, + request: acp_old::PushToolCallParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.upsert_tool_call( + into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), + cx, + ) + }) + })?? + .context("Failed to update thread")?; + + Ok(acp_old::PushToolCallResponse { + id: acp_old::ToolCallId(old_acp_id), + }) + } + + async fn update_tool_call( + &self, + request: acp_old::UpdateToolCallParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_tool_call( + acp::ToolCallUpdate { + id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(into_new_tool_call_status(request.status)), + content: Some( + request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect::>(), + ), + ..Default::default() + }, + }, + cx, + ) + }) + })? + .context("Failed to update thread")??; + + Ok(()) + } + + async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_plan( + acp::Plan { + entries: request + .entries + .into_iter() + .map(into_new_plan_entry) + .collect(), + }, + cx, + ) + }) + })? + .context("Failed to update thread")?; + + Ok(()) + } + + async fn read_text_file( + &self, + acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, + ) -> Result { + let content = self + .cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.read_text_file(path, line, limit, false, cx) + }) + })? + .context("Failed to update thread")? + .await?; + Ok(acp_old::ReadTextFileResponse { content }) + } + + async fn write_text_file( + &self, + acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, + ) -> Result<(), acp_old::Error> { + self.cx + .update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) + })? + .context("Failed to update thread")? + .await?; + + Ok(()) + } +} + +fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { + acp::ToolCall { + id, + title: request.label, + kind: acp_kind_from_old_icon(request.icon), + status: acp::ToolCallStatus::InProgress, + content: request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect(), + locations: request + .locations + .into_iter() + .map(into_new_tool_call_location) + .collect(), + raw_input: None, + raw_output: None, + } +} + +fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { + match icon { + acp_old::Icon::FileSearch => acp::ToolKind::Search, + acp_old::Icon::Folder => acp::ToolKind::Search, + acp_old::Icon::Globe => acp::ToolKind::Search, + acp_old::Icon::Hammer => acp::ToolKind::Other, + acp_old::Icon::LightBulb => acp::ToolKind::Think, + acp_old::Icon::Pencil => acp::ToolKind::Edit, + acp_old::Icon::Regex => acp::ToolKind::Search, + acp_old::Icon::Terminal => acp::ToolKind::Execute, + } +} + +fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { + match status { + acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, + acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, + acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, + } +} + +fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { + match content { + acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), + acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { + diff: into_new_diff(diff), + }, + } +} + +fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { + acp::Diff { + path: diff.path, + old_text: diff.old_text, + new_text: diff.new_text, + } +} + +fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { + acp::ToolCallLocation { + path: location.path, + line: location.line, + } +} + +fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { + acp::PlanEntry { + content: entry.content, + priority: into_new_plan_priority(entry.priority), + status: into_new_plan_status(entry.status), + } +} + +fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { + match priority { + acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, + acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, + acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, + } +} + +fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { + match status { + acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, + acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, + acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, + } +} + +pub struct AcpConnection { + pub name: &'static str, + pub connection: acp_old::AgentConnection, + pub _child_status: Task>, + pub current_thread: Rc>>, +} + +impl AcpConnection { + pub fn stdio( + name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Task> { + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |cx| { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + log::trace!("Spawned (pid: {})", child.id()); + + let foreground_executor = cx.foreground_executor().clone(); + + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), + stdin, + stdout, + move |fut| foreground_executor.spawn(fut).detach(), + ); + + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); + + let child_status = cx.background_spawn(async move { + let result = match child.status().await { + Err(e) => Err(anyhow!(e)), + Ok(result) if result.success() => Ok(()), + Ok(result) => Err(anyhow!(result)), + }; + drop(io_task); + result + }); + + Ok(Self { + name, + connection, + _child_status: child_status, + current_thread: thread_rc, + }) + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut App, + ) -> Task>> { + let task = self.connection.request_any( + acp_old::InitializeParams { + protocol_version: acp_old::ProtocolVersion::latest(), + } + .into_any(), + ); + let current_thread = self.current_thread.clone(); + cx.spawn(async move |cx| { + let result = task.await?; + let result = acp_old::InitializeParams::response_from_any(result)?; + + if !result.is_authenticated { + anyhow::bail!(AuthRequired::new()) + } + + cx.update(|cx| { + let thread = cx.new(|cx| { + let session_id = acp::SessionId("acp-old-no-id".into()); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new(self.name, self.clone(), project, action_log, session_id) + }); + current_thread.replace(thread.downgrade()); + thread + }) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let task = self + .connection + .request_any(acp_old::AuthenticateParams.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let chunks = params + .prompt + .into_iter() + .filter_map(|block| match block { + acp::ContentBlock::Text(text) => { + Some(acp_old::UserMessageChunk::Text { text: text.text }) + } + acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { + path: link.uri.into(), + }), + _ => None, + }) + .collect(); + + let task = self + .connection + .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + 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 + .request_any(acp_old::CancelSendMessageParams.into_any()); + cx.foreground_executor() + .spawn(async move { + task.await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs new file mode 100644 index 0000000000..29f389547d --- /dev/null +++ b/crates/agent_servers/src/acp/v1.rs @@ -0,0 +1,367 @@ +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::path::Path; +use std::rc::Rc; +use std::{any::Any, cell::RefCell}; + +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::{AgentServerCommand, acp::UnsupportedVersion}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; + +pub struct AcpConnection { + server_name: &'static str, + 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; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + 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 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()); + } + + Ok(Self { + auth_methods: response.auth_methods, + connection: connection.into(), + 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, + self.clone(), + project, + action_log, + session_id.clone(), + ) + })?; + + 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") + { + 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(), + }; + 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/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 7c7e124ca7..2f5ec478ae 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,6 +1,5 @@ mod acp; mod claude; -mod custom; mod gemini; mod settings; @@ -8,7 +7,6 @@ mod settings; pub mod e2e_tests; pub use claude::*; -pub use custom::*; pub use gemini::*; pub use settings::*; @@ -33,10 +31,9 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> SharedString; - fn empty_state_headline(&self) -> SharedString; - fn empty_state_message(&self) -> SharedString; - fn telemetry_id(&self) -> &'static str; + fn name(&self) -> &'static str; + fn empty_state_headline(&self) -> &'static str; + fn empty_state_message(&self) -> &'static str; fn connect( &self, @@ -98,7 +95,7 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub async fn resolve( + pub(crate) 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 250e564526..ef666974f1 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, SharedString, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::{ResultExt, debug_panic}; @@ -43,20 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn telemetry_id(&self) -> &'static str { - "claude-code" + fn name(&self) -> &'static str { + "Claude Code" } - fn name(&self) -> SharedString { - "Claude Code".into() - } - - fn empty_state_headline(&self) -> SharedString { + fn empty_state_headline(&self) -> &'static str { self.name() } - fn empty_state_message(&self) -> SharedString { - "How can I help you today?".into() + fn empty_state_message(&self) -> &'static str { + "How can I help you today?" } fn logo(&self) -> ui::IconName { @@ -253,19 +249,13 @@ 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, ) })?; @@ -329,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs deleted file mode 100644 index 72823026d7..0000000000 --- a/crates/agent_servers/src/custom.rs +++ /dev/null @@ -1,63 +0,0 @@ -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 42264b4b4f..c271079071 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,15 +1,17 @@ -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; +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 util::path; pub async fn test_basic(server: F, cx: &mut TestAppContext) @@ -477,7 +479,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { 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 5d6a70fa64..3b892e7931 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,10 +4,11 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{Entity, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; +use ui::App; use crate::AllAgentServersSettings; @@ -17,20 +18,16 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn telemetry_id(&self) -> &'static str { - "gemini-cli" + fn name(&self) -> &'static str { + "Gemini CLI" } - fn name(&self) -> SharedString { - "Gemini CLI".into() + fn empty_state_headline(&self) -> &'static str { + "Welcome to Gemini CLI" } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "Ask questions, edit files, run commands".into() + fn empty_state_message(&self) -> &'static str { + "Ask questions, edit files, run commands" } fn logo(&self) -> ui::IconName { @@ -57,7 +54,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: Self::install_command().into(), + install_command: "npm install -g @google/gemini-cli@preview".into() }.into()); }; @@ -92,7 +89,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: Self::upgrade_command().into(), + upgrade_command: "npm install -g @google/gemini-cli@preview".into(), }.into()) } } @@ -105,20 +102,6 @@ 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::*; diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 96ac6e3cbe..645674b5f1 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,7 +1,6 @@ use crate::AgentServerCommand; use anyhow::Result; -use collections::HashMap; -use gpui::{App, SharedString}; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -14,13 +13,9 @@ 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, PartialEq)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -34,26 +29,13 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { - gemini, - claude, - custom, - } in sources.defaults_and_customizations() - { + for AllAgentServersSettings { gemini, claude } 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_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6b0979ee69..43e3b25124 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -67,7 +67,6 @@ 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 5b40967069..3587e5144e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let uri = MentionUri::Symbol { - abs_path, + 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(); @@ -805,7 +805,7 @@ pub(crate) fn search_threads( history_store: &Entity, cx: &mut App, ) -> Task> { - let threads = history_store.read(cx).entries().collect(); + let threads = history_store.read(cx).entries(cx); if query.is_empty() { return Task::ready(threads); } diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index becf6953fd..c310473259 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,12 +1,12 @@ -use std::{cell::Cell, ops::Range, rc::Rc}; +use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent_client_protocol::{PromptCapabilities, ToolCallId}; +use agent_client_protocol::ToolCallId; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; @@ -27,7 +27,6 @@ pub struct EntryViewState { prompt_store: Option>, entries: Vec, prevent_slash_commands: bool, - prompt_capabilities: Rc>, } impl EntryViewState { @@ -36,7 +35,6 @@ impl EntryViewState { project: Entity, history_store: Entity, prompt_store: Option>, - prompt_capabilities: Rc>, prevent_slash_commands: bool, ) -> Self { Self { @@ -46,7 +44,6 @@ impl EntryViewState { prompt_store, entries: Vec::new(), prevent_slash_commands, - prompt_capabilities, } } @@ -84,7 +81,6 @@ 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 { @@ -154,22 +150,10 @@ impl EntryViewState { }); } } - AgentThreadEntry::AssistantMessage(message) => { - let entry = if let Some(Entry::AssistantMessage(entry)) = - self.entries.get_mut(index) - { - entry - } else { - self.set_entry( - index, - Entry::AssistantMessage(AssistantMessageEntry::default()), - ); - let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { - unreachable!() - }; - entry - }; - entry.sync(message); + AgentThreadEntry::AssistantMessage(_) => { + if index == self.entries.len() { + self.entries.push(Entry::empty()) + } } }; } @@ -189,7 +173,7 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { - Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} + Entry::UserMessage { .. } => {} Entry::Content(response_views) => { for view in response_views.values() { if let Ok(diff_editor) = view.clone().downcast::() { @@ -220,29 +204,9 @@ pub enum ViewEvent { MessageEditorEvent(Entity, MessageEditorEvent), } -#[derive(Default, Debug)] -pub struct AssistantMessageEntry { - scroll_handles_by_chunk_index: HashMap, -} - -impl AssistantMessageEntry { - pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { - self.scroll_handles_by_chunk_index.get(&ix).cloned() - } - - pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { - if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { - let ix = message.chunks.len() - 1; - let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); - handle.scroll_to_bottom(); - } - } -} - #[derive(Debug)] pub enum Entry { UserMessage(Entity), - AssistantMessage(AssistantMessageEntry), Content(HashMap), } @@ -250,7 +214,7 @@ impl Entry { pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, + Entry::Content(_) => None, } } @@ -271,16 +235,6 @@ impl Entry { .map(|entity| entity.downcast::().unwrap()) } - pub fn scroll_handle_for_assistant_message_chunk( - &self, - chunk_ix: usize, - ) -> Option { - match self { - Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), - Self::UserMessage(_) | Self::Content(_) => None, - } - } - fn content_map(&self) -> Option<&HashMap> { match self { Self::Content(map) => Some(map), @@ -296,7 +250,7 @@ impl Entry { pub fn has_content(&self) -> bool { match self { Self::Content(map) => !map.is_empty(), - Self::UserMessage(_) | Self::AssistantMessage(_) => false, + Self::UserMessage(_) => false, } } } @@ -449,7 +403,6 @@ 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 12ae893c31..dc31c5fe10 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,37 +6,36 @@ use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent2::HistoryStore; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, 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, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; use futures::{ - FutureExt as _, - future::{Shared, join_all}, + FutureExt as _, TryFutureExt as _, + future::{Shared, join_all, try_join_all}, }; use gpui::{ - Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, + HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, + UnderlineStyle, WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use postage::stream::Stream as _; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{PromptId, PromptStore}; +use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{ cell::Cell, ffi::OsStr, fmt::Write, - ops::{Range, RangeInclusive}, + ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -45,13 +44,17 @@ use std::{ use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ - 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, + 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, +}; +use url::Url; +use util::ResultExt; +use workspace::{ + Toast, Workspace, + notifications::{NotificationId, NotifyResultExt as _}, }; -use util::{ResultExt, debug_panic}; -use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -74,7 +77,6 @@ pub enum MessageEditorEvent { Send, Cancel, Focus, - LostFocus, } impl EventEmitter for MessageEditor {} @@ -85,7 +87,6 @@ impl MessageEditor { project: Entity, history_store: Entity, prompt_store: Option>, - prompt_capabilities: Rc>, placeholder: impl Into>, prevent_slash_commands: bool, mode: EditorMode, @@ -99,6 +100,7 @@ impl MessageEditor { }, None, ); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let completion_provider = ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), @@ -132,21 +134,17 @@ impl MessageEditor { editor }); - cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { + cx.on_focus(&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(); - 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 { + 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 { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -154,12 +152,9 @@ impl MessageEditor { cx, ); } - let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); - this.mention_set.remove_invalid(snapshot); - cx.notify(); } - } - })); + })); + } Self { editor, @@ -205,6 +200,10 @@ impl MessageEditor { .detach(); } + pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) { + self.prompt_capabilities.set(capabilities); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity { &self.editor @@ -221,9 +220,9 @@ impl MessageEditor { pub fn mentions(&self) -> HashSet { self.mention_set - .mentions + .uri_by_crease_id .values() - .map(|(uri, _)| uri.clone()) + .cloned() .collect() } @@ -248,172 +247,132 @@ impl MessageEditor { else { return Task::ready(()); }; - let end_anchor = snapshot - .buffer_snapshot - .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - 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 { + 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") { + if !self.prompt_capabilities.get().image { + struct ImagesNotAllowed; + + let end_anchor = snapshot.buffer_snapshot.anchor_before( + start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, + ); + + self.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([((start_anchor..end_anchor), "")], cx); + }); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "This agent does not support images yet", + ) + .autohide(), + cx, + ); + }) + .ok(); + return Task::ready(()); + } + + 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, + start_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, + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) else { return Task::ready(()); }; - 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" - ))) + match mention_uri { + MentionUri::Fetch { url } => { + self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) } - MentionUri::Selection { .. } => { - // Handled elsewhere - debug_panic!("unexpected selection URI"); - Task::ready(Err(anyhow!("unexpected selection URI"))) + MentionUri::Directory { abs_path } => { + self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, 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::Thread { id, name } => { + self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) } - }) - } - - 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"))); + MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( + crease_id, + start_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(()) } - 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(); @@ -428,21 +387,24 @@ 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(Err(anyhow!("project path not found"))); + return Task::ready(()); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(Err(anyhow!("project entry not found"))); + return Task::ready(()); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(Err(anyhow!("worktree not found"))); + return Task::ready(()); }; let project = self.project.clone(); - cx.spawn(async move |_, cx| { + let task = cx.spawn(async move |_, cx| { let directory_path = entry.path.clone(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; @@ -492,83 +454,89 @@ impl MessageEditor { ((rel_path, full_path, rope), buffer) }) .unzip(); - Mention::Text { - content: render_directory_contents(contents), - tracked_buffers, - } + (render_directory_contents(contents), tracked_buffers) }) .await; anyhow::Ok(contents) + }); + let task = cx + .spawn(async move |_, _| task.await.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 http_client = match self + ) -> Task<()> { + let Some(http_client) = self .workspace - .update(cx, |workspace, _| workspace.client().http_client()) - { - Ok(http_client) => http_client, - Err(e) => return Task::ready(Err(e)), - }; - 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(), - }) - }) - } - - 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) + .update(cx, |workspace, _cx| workspace.client().http_client()) + .ok() else { - return Task::ready(Err(anyhow!("project path not found"))); + return Task::ready(()); }; - 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(), + 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 }) + .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); + } + }) + .ok(); }) } @@ -593,24 +561,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let abs_path = buffer - .read(cx) - .project_path(cx) - .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)); + // TODO support selections from buffers with no path + let Some(project_path) = buffer.read(cx).project_path(cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; 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 { - abs_path: abs_path.clone(), + path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(abs_path.as_deref(), &line_range).into(), + selection_name(&abs_path, &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -622,69 +590,132 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set.mentions.insert( - crease_id, - ( - uri, - Task::ready(Ok(Mention::Text { - content: text, - tracked_buffers: vec![buffer], - })) - .shared(), - ), - ); + self.mention_set.insert_uri(crease_id, uri); } } fn confirm_mention_for_thread( &mut self, + crease_id: CreaseId, + anchor: Anchor, id: acp::SessionId, + name: String, + window: &mut Window, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; 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); - 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(), - }) + 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()); + self.mention_set.insert_uri(crease_id, uri); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + 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); + this.mention_set.uri_by_crease_id.remove(&crease_id); + }) + .ok(); + } }) } fn confirm_mention_for_text_thread( &mut self, + crease_id: CreaseId, + anchor: Anchor, path: PathBuf, + name: String, + window: &mut Window, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; let context = self.history_store.update(cx, |text_thread_store, cx| { text_thread_store.load_text_thread(path.as_path().into(), cx) }); - 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(), + 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) }) + .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, Vec>)>> { - let contents = self - .mention_set - .contents(&self.prompt_capabilities.get(), cx); + let contents = self.mention_set.contents( + &self.project, + self.prompt_store.as_ref(), + &self.prompt_capabilities.get(), + window, + cx, + ); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -699,7 +730,12 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - let Some((uri, mention)) = contents.get(&crease_id) else { + // 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 { continue; }; @@ -717,6 +753,7 @@ impl MessageEditor { } let chunk = match mention { Mention::Text { + uri, content, tracked_buffers, } => { @@ -733,25 +770,17 @@ 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, + uri: mention_image + .abs_path + .as_ref() + .map(|path| format!("file://{}", path.display())), }) } - Mention::UriOnly => { + Mention::UriOnly(uri) => { acp::ContentBlock::ResourceLink(acp::ResourceLink { name: uri.name(), uri: uri.to_uri().to_string(), @@ -790,13 +819,7 @@ 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 - .mentions - .drain() - .map(|(crease_id, _)| crease_id), - cx, - ) + editor.remove_creases(self.mention_set.drain(), cx) }); } @@ -836,7 +859,7 @@ impl MessageEditor { } cx.stop_propagation(); - let replacement_text = MentionUri::PastedImage.as_link().to_string(); + let replacement_text = "image"; for image in images { let (excerpt_id, text_anchor, multibuffer_anchor) = self.editor.update(cx, |message_editor, cx| { @@ -859,64 +882,24 @@ impl MessageEditor { }); let content_len = replacement_text.len(); - let Some(start_anchor) = multibuffer_anchor else { - continue; + let Some(anchor) = multibuffer_anchor else { + return; }; - 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( + let task = Task::ready(Ok(Arc::new(image))).shared(); + let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, - MentionUri::PastedImage.name().into(), - IconName::Image.path().into(), - Some(Task::ready(Ok(image.clone())).shared()), + None.clone(), + task.clone(), self.editor.clone(), window, cx, ) else { - continue; + return; }; - 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(); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) + .detach(); } } @@ -1018,6 +1001,67 @@ 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?; + 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); @@ -1035,6 +1079,7 @@ impl MessageEditor { let mut text = String::new(); let mut mentions = Vec::new(); + let mut images = Vec::new(); for chunk in message { match chunk { @@ -1045,60 +1090,22 @@ 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, Mention::UriOnly)); + mentions.push((start..end, mention_uri, resource.text)); } } - 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; - }; + acp::ContentBlock::Image(content) => { let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); + text.push_str("image"); let end = text.len(); - mentions.push(( - start..end, - mention_uri, - Mention::Image(MentionImage { - data: data.into(), - format, - }), - )); + images.push((start..end, content)); } - acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} + acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) + | acp::ContentBlock::ResourceLink(_) => {} } } @@ -1107,27 +1114,88 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri, mention) in mentions { + for (range, mention_uri, text) in mentions { let anchor = snapshot.anchor_before(range.start); - let Some((crease_id, tx)) = insert_crease_for_mention( + let crease_id = crate::context_picker::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, - ) else { + ); + + 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, Vec::new()))).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 { continue; }; - drop(tx); + 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())); - self.mention_set.mentions.insert( - crease_id, - (mention_uri.clone(), Task::ready(Ok(mention)).shared()), + 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, ); + 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(); } @@ -1174,16 +1242,17 @@ 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) + } } fn render_directory_contents(entries: Vec<(Arc, PathBuf, String)>) -> String { @@ -1240,21 +1309,23 @@ impl Render for MessageEditor { } } -pub(crate) fn insert_crease_for_mention( +pub(crate) fn insert_crease_for_image( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, - crease_label: SharedString, - crease_icon: SharedString, - // abs_path: Option>, - image: Option, String>>>>, + abs_path: Option>, + image: Shared, String>>>, editor: Entity, window: &mut Window, cx: &mut App, -) -> Option<(CreaseId, postage::barrier::Sender)> { - let (tx, rx) = postage::barrier::channel(); +) -> 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")); - let crease_id = editor.update(cx, |editor, cx| { + editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; @@ -1263,15 +1334,7 @@ pub(crate) fn insert_crease_for_mention( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_fold_icon_button( - crease_label, - crease_icon, - start..end, - rx, - image, - cx.weak_entity(), - cx, - ), + render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), merge_adjacent: false, ..Default::default() }; @@ -1288,112 +1351,63 @@ pub(crate) fn insert_crease_for_mention( editor.fold_creases(vec![crease], false, window, cx); Some(ids[0]) - })?; - - Some((crease_id, tx)) + }) } -fn render_fold_icon_button( +fn render_image_fold_icon_button( label: SharedString, - icon: SharedString, - range: Range, - mut loading_finished: postage::barrier::Receiver, - image_task: Option, String>>>>, + image_task: Shared, String>>>, editor: WeakEntity, - cx: &mut App, ) -> Arc, &mut App) -> AnyElement> { - 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()) -} + Arc::new({ + 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(); -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(); + 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(); - 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() + 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() - } - }), - ) - } + .into_any_element() + } + }) } struct ImageHover { @@ -1411,62 +1425,283 @@ impl Render for ImageHover { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum Mention { Text { + uri: MentionUri, content: String, tracked_buffers: Vec>, }, Image(MentionImage), - UriOnly, + UriOnly(MentionUri), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { + pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } #[derive(Default)] pub struct MentionSet { - mentions: HashMap>>)>, + uri_by_crease_id: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, + thread_summaries: HashMap>>>, + text_thread_summaries: HashMap>>>, + directories: HashMap>), String>>>>, } impl MentionSet { - fn contents( + 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.directories.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn contents( &self, + project: &Entity, + prompt_store: Option<&Entity>, prompt_capabilities: &acp::PromptCapabilities, + _window: &mut Window, cx: &mut App, - ) -> Task>> { + ) -> Task>> { if !prompt_capabilities.embedded_context { let mentions = self - .mentions + .uri_by_crease_id .iter() - .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly))) + .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) .collect(); return Task::ready(Ok(mentions)); } - let mentions = self.mentions.clone(); - cx.spawn(async move |_cx| { - 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) - }) - } + let mut processed_image_creases = HashSet::default(); - 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); + 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, + tracked_buffers: vec![buffer], + }, + )) + }) + } + 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 |_| { + let (content, tracked_buffers) = + content.await.map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers, + }, + )) + }) + } + 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, + tracked_buffers: vec![buffer], + }, + )) + }) + } + 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(), + tracked_buffers: Vec::new(), + }, + )) + }) + } + 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}"))?, + tracked_buffers: Vec::new(), + }, + )) + }) + } + 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, + tracked_buffers: Vec::new(), + }, + )) + }) + } + 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}"))?, + tracked_buffers: Vec::new(), + }, + )) + }) + } + } + }) + .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}"))?), + )) + })) + })); + + cx.spawn(async move |_cx| { + let contents = try_join_all(contents).await?.into_iter().collect(); + anyhow::Ok(contents) + }) } } @@ -1609,7 +1844,7 @@ impl Addon for MessageEditorAddon { #[cfg(test)] mod tests { - use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc}; + use std::{ops::Range, path::Path, sync::Arc}; use acp_thread::MentionUri; use agent_client_protocol as acp; @@ -1655,7 +1890,6 @@ mod tests { project.clone(), history_store.clone(), None, - Default::default(), "Test", false, EditorMode::AutoHeight { @@ -1726,7 +1960,9 @@ mod tests { }); let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)) + .update_in(cx, |message_editor, window, cx| { + message_editor.contents(window, cx) + }) .await .unwrap(); @@ -1793,8 +2029,7 @@ mod tests { "six.txt": "6", "seven.txt": "7", "eight.txt": "8", - }, - "x.png": "", + } }), ) .await; @@ -1845,7 +2080,6 @@ 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(); @@ -1855,7 +2089,6 @@ mod tests { project.clone(), history_store.clone(), None, - prompt_capabilities.clone(), "Test", false, EditorMode::AutoHeight { @@ -1900,10 +2133,13 @@ mod tests { editor.set_text("", window, cx); }); - prompt_capabilities.set(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, + message_editor.update(&mut cx, |editor, _cx| { + // Enable all prompt capabilities + editor.set_prompt_capabilities(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }); }); cx.simulate_input("Lorem "); @@ -1978,10 +2214,14 @@ mod tests { }; let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -1989,7 +2229,7 @@ mod tests { .collect::>(); { - let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -1997,10 +2237,14 @@ mod tests { } let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&acp::PromptCapabilities::default(), cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &acp::PromptCapabilities::default(), + window, + cx, + ) }) .await .unwrap() @@ -2008,7 +2252,7 @@ mod tests { .collect::>(); { - let [(uri, Mention::UriOnly)] = contents.as_slice() else { + let [Mention::UriOnly(uri)] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); @@ -2048,10 +2292,14 @@ mod tests { cx.run_until_parked(); let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2061,7 +2309,7 @@ mod tests { let url_eight = uri!("file:///dir/b/eight.txt"); { - let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "8"); @@ -2158,10 +2406,14 @@ mod tests { }); let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .contents(&all_prompt_capabilities, cx) + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2169,7 +2421,7 @@ mod tests { .collect::>(); { - let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { + let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2184,85 +2436,11 @@ mod tests { 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) ") - ); - }); - - // 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); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); } fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index a49dae25b3..d76969378c 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; -use fuzzy::StringMatchCandidate; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; -use std::{fmt::Display, ops::Range}; -use text::Bias; +use std::{fmt::Display, ops::Range, sync::Arc}; use time::{OffsetDateTime, UtcOffset}; use ui::{ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, prelude::*, }; +use util::ResultExt; pub struct AcpThreadHistory { pub(crate) history_store: Entity, @@ -22,38 +22,38 @@ pub struct AcpThreadHistory { selected_index: usize, hovered_index: Option, search_editor: Entity, - search_query: SharedString, - - visible_items: Vec, - + 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, scrollbar_visibility: bool, scrollbar_state: ScrollbarState, local_timezone: UtcOffset, - - _update_task: Task<()>, _subscriptions: Vec, } +enum SearchState { + Empty, + Searching { + query: SharedString, + _task: Task<()>, + }, + Searched { + query: SharedString, + matches: Vec, + }, +} + enum ListItemType { BucketSeparator(TimeBucket), Entry { - entry: HistoryEntry, + index: usize, 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 { @@ -78,15 +78,12 @@ impl AcpThreadHistory { cx.subscribe(&search_editor, |this, search_editor, event, cx| { if let EditorEvent::BufferEdited = event { let query = search_editor.read(cx).text(cx); - if this.search_query != query { - this.search_query = query.into(); - this.update_visible_items(false, cx); - } + this.search(query.into(), cx); } }); let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_visible_items(true, cx); + this.update_all_entries(cx); }); let scroll_handle = UniformListScrollHandle::default(); @@ -97,7 +94,10 @@ impl AcpThreadHistory { scroll_handle, selected_index: 0, hovered_index: None, - visible_items: Default::default(), + search_state: SearchState::Empty, + all_entries: Default::default(), + separated_items: Default::default(), + separated_item_indexes: Default::default(), search_editor, scrollbar_visibility: true, scrollbar_state, @@ -105,61 +105,29 @@ impl AcpThreadHistory { chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), - search_query: SharedString::default(), _subscriptions: vec![search_editor_subscription, history_store_subscription], - _update_task: Task::ready(()), + _separated_items_task: None, }; - this.update_visible_items(false, cx); + this.update_all_entries(cx); this } - fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { - let entries = self + fn update_all_entries(&mut self, cx: &mut Context) { + let new_entries: Arc> = self .history_store - .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 - }; + .update(cx, |store, cx| store.entries(cx)) + .into(); - 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 - }; + self._separated_items_task.take(); - this.visible_items = new_visible_items; - this.set_selected_index(new_selected_index, Bias::Right, cx); - cx.notify(); - }) - .ok(); - }); - } + let mut items = Vec::with_capacity(new_entries.len() + 1); + let mut indexes = Vec::with_capacity(new_entries.len() + 1); - 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 bg_task = cx.background_spawn(async move { let mut bucket = None; let today = Local::now().naive_local().date(); - for entry in entries.into_iter() { + for (index, entry) in new_entries.iter().enumerate() { let entry_date = entry .updated_at() .with_timezone(&Local) @@ -172,33 +140,75 @@ impl AcpThreadHistory { items.push(ListItemType::BucketSeparator(entry_bucket)); } + indexes.push(items.len() as u32); items.push(ListItemType::Entry { - entry, + index, format: entry_bucket.into(), }); } - items - }) + (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); } - fn filter_search_results( - &self, - entries: Vec, - cx: &App, - ) -> Task> { - let query = self.search_query.clone(); - cx.background_spawn({ + 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(); let executor = cx.background_executor().clone(); async move { - let mut candidates = Vec::with_capacity(entries.len()); + let mut candidates = Vec::with_capacity(all_entries.len()); - for (idx, entry) in entries.iter().enumerate() { + for (idx, entry) in all_entries.iter().enumerate() { candidates.push(StringMatchCandidate::new(idx, entry.title())); } const MAX_MATCHES: usize = 100; - let matches = fuzzy::match_strings( + fuzzy::match_strings( &candidates, &query, false, @@ -207,61 +217,74 @@ impl AcpThreadHistory { &Default::default(), executor, ) - .await; - - matches - .into_iter() - .map(|search_match| ListItemType::SearchResult { - entry: entries[search_match.candidate_id].clone(), - positions: search_match.positions, - }) - .collect() + .await } - }) + }); + + 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 { - self.visible_items.is_empty() && !self.search_query.is_empty() - } - - 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; + match &self.search_state { + SearchState::Empty => false, + SearchState::Searching { .. } => false, + SearchState::Searched { matches, .. } => matches.is_empty(), } - 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 - } - } - }; + } + + 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)), } - self.selected_index = index; - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Top); - cx.notify() } pub fn select_previous( @@ -270,10 +293,13 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - 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); + 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); + } } } @@ -283,10 +309,13 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - 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); + 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); + } } } @@ -296,47 +325,35 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - self.set_selected_index(0, Bias::Right, cx); + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(0, cx); + } } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(count - 1, cx); + } } - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - self.confirm_entry(self.selected_index, cx); - } + fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context) { + self.selected_index = entry_index; - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_history_entry(ix) else { - return; - }; - cx.emit(ThreadHistoryEvent::Open(entry.clone())); - } - - fn remove_selected_thread( - &mut self, - _: &RemoveSelectedThread, - _window: &mut Window, - cx: &mut Context, - ) { - self.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { - let Some(entry) = self.get_history_entry(visible_item_ix) else { - return; + 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, }; - let task = match entry { - HistoryEntry::AcpThread(thread) => self - .history_store - .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(context.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); + self.scroll_handle + .scroll_to_item(scroll_ix, ScrollStrategy::Top); + + cx.notify(); } fn render_scrollbar(&self, cx: &mut Context) -> Option> { @@ -376,33 +393,91 @@ impl AcpThreadHistory { ) } - fn render_list_items( + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + self.confirm_entry(self.selected_index, cx); + } + + fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { + let Some(entry) = self.get_match(ix) else { + return; + }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } + + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context, + ) { + 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 { + return; + }; + + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } + + fn list_items( &mut self, range: Range, _window: &mut Window, cx: &mut Context, ) -> Vec { - self.visible_items - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) - .collect() + 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![] + } + } } - fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { + fn render_list_item( + &self, + item: &ListItemType, + highlight_positions: Vec, + cx: &Context, + ) -> AnyElement { match item { - 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::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::BucketSeparator(bucket) => div() .px(DynamicSpacing::Base06.rems(cx)) .pt_2() @@ -420,12 +495,12 @@ impl AcpThreadHistory { &self, entry: &HistoryEntry, format: EntryTimeFormat, - ix: usize, + list_entry_ix: usize, highlight_positions: Vec, cx: &Context, ) -> AnyElement { - let selected = ix == self.selected_index; - let hovered = Some(ix) == self.hovered_index; + let selected = list_entry_ix == self.selected_index; + let hovered = Some(list_entry_ix) == self.hovered_index; let timestamp = entry.updated_at().timestamp(); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); @@ -433,7 +508,7 @@ impl AcpThreadHistory { .w_full() .pb_1() .child( - ListItem::new(ix) + ListItem::new(list_entry_ix) .rounded() .toggle_state(selected) .spacing(ListItemSpacing::Sparse) @@ -455,14 +530,14 @@ impl AcpThreadHistory { ) .on_hover(cx.listener(move |this, is_hovered, _window, cx| { if *is_hovered { - this.hovered_index = Some(ix); - } else if this.hovered_index == Some(ix) { + this.hovered_index = Some(list_entry_ix); + } else if this.hovered_index == Some(list_entry_ix) { this.hovered_index = None; } cx.notify(); })) - .end_slot::(if hovered { + .end_slot::(if hovered || selected { Some( IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) @@ -471,14 +546,16 @@ impl AcpThreadHistory { .tooltip(move |window, cx| { Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) }) - .on_click( - cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)), - ), + .on_click(cx.listener(move |this, _, _, cx| { + this.remove_thread(list_entry_ix, cx) + })), ) } else { None }) - .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), + .on_click( + cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), + ), ) .into_any_element() } @@ -501,7 +578,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.history_store.read(cx).is_empty(cx), |parent| { + .when(!self.all_entries.is_empty(), |parent| { parent.child( h_flex() .h(px(41.)) // Match the toolbar perfectly @@ -527,7 +604,7 @@ impl Render for AcpThreadHistory { .overflow_hidden() .flex_grow(); - if self.history_store.read(cx).is_empty(cx) { + if self.all_entries.is_empty() { view.justify_center() .child( h_flex().w_full().justify_center().child( @@ -546,9 +623,9 @@ impl Render for AcpThreadHistory { .child( uniform_list( "thread-history", - self.visible_items.len(), + self.list_item_count(), cx.processor(|this, range: Range, window, cx| { - this.render_list_items(range, window, cx) + this.list_items(range, window, cx) }), ) .p_1() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c68c3a3e93..7e330b7e6f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,7 +5,7 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent_client_protocol::{self as acp, PromptCapabilities}; +use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; @@ -15,16 +15,16 @@ use buffer_diff::BufferDiff; use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -34,8 +34,6 @@ 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}; @@ -43,7 +41,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, + Scrollbar, ScrollbarState, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -58,14 +56,13 @@ use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::ui::preview::UsageCallout; -use crate::ui::{ - AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, -}; +use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, 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; @@ -258,7 +255,6 @@ pub struct AcpThreadView { hovered_recent_history_item: Option, entry_view_state: Entity, message_editor: Entity, - focus_handle: FocusHandle, model_selector: Option>, profile_selector: Option>, notifications: Vec>, @@ -274,11 +270,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - should_be_following: bool, editing_message: Option, - prompt_capabilities: Rc>, - is_loading_contents: bool, - install_command_markdown: Entity, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -289,8 +281,7 @@ enum ThreadState { }, Ready { thread: Entity, - title_editor: Option>, - _subscriptions: Vec, + _subscription: [Subscription; 2], }, LoadError(LoadError), Unauthenticated { @@ -314,23 +305,14 @@ 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| { let mut editor = MessageEditor::new( workspace.clone(), project.clone(), history_store.clone(), prompt_store.clone(), - prompt_capabilities.clone(), - placeholder, + "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -353,7 +335,6 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), - prompt_capabilities.clone(), prevent_slash_commands, ) }); @@ -387,15 +368,10 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, - should_be_following: false, history_store, hovered_recent_history_item: None, - prompt_capabilities, - is_loading_contents: false, - install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), _subscriptions: subscriptions, _cancel_task: None, - focus_handle: cx.focus_handle(), } } @@ -419,12 +395,8 @@ impl AcpThreadView { let connection = match connect_task.await { Ok(connection) => connection, Err(err) => { - 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); - } + this.update(cx, |this, cx| { + this.handle_load_error(err, cx); cx.notify(); }) .log_err(); @@ -473,10 +445,12 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { match result { Ok(thread) => { - let action_log = thread.read(cx).action_log().clone(); + let thread_subscription = + cx.subscribe_in(&thread, window, Self::handle_thread_event); - this.prompt_capabilities - .set(thread.read(cx).prompt_capabilities()); + let action_log = thread.read(cx).action_log().clone(); + let action_log_subscription = + cx.observe(&action_log, |_, _, cx| cx.notify()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -515,33 +489,10 @@ 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, - title_editor, - _subscriptions: subscriptions, + _subscription: [thread_subscription, action_log_subscription], }; - this.message_editor.focus_handle(cx).focus(window); this.profile_selector = this.as_native_thread(cx).map(|thread| { cx.new(|cx| { @@ -554,10 +505,15 @@ impl AcpThreadView { }) }); + this.message_editor.update(cx, |message_editor, _cx| { + message_editor + .set_prompt_capabilities(connection.prompt_capabilities()); + }); + cx.notify(); } Err(err) => { - this.handle_load_error(err, window, cx); + this.handle_load_error(err, cx); } }; }) @@ -604,7 +560,7 @@ impl AcpThreadView { let view = registry.read(cx).provider(&provider_id).map(|provider| { provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), + language_model::ConfigurationViewTargetAgent::Other(agent_name), window, cx, ) @@ -626,28 +582,17 @@ 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, - window: &mut Window, - cx: &mut Context, - ) { + fn handle_load_error(&mut self, err: anyhow::Error, 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(); } @@ -664,24 +609,12 @@ impl AcpThreadView { } } - pub fn title(&self) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { match &self.thread_state { - ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), + ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Loading { .. } => "Loading…".into(), - ThreadState::LoadError(error) => match error { - LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), - LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), - }, - } - } - - pub fn title_editor(&self) -> Option> { - if let ThreadState::Ready { title_editor, .. } = &self.thread_state { - title_editor.clone() - } else { - None + ThreadState::LoadError(_) => "Failed to load".into(), + ThreadState::Unauthenticated { .. } => "Authentication Required".into(), } } @@ -729,35 +662,6 @@ 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, @@ -771,7 +675,6 @@ impl AcpThreadView { MessageEditorEvent::Focus => { self.cancel_editing(&Default::default(), window, cx); } - MessageEditorEvent::LostFocus => {} } } @@ -803,18 +706,6 @@ impl AcpThreadView { 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); } @@ -829,9 +720,6 @@ 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| { @@ -848,11 +736,6 @@ 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()), @@ -867,7 +750,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); self.send_impl(contents, window, cx) } @@ -880,7 +763,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); cx.spawn_in(window, async move |this, cx| { cancelled.await; @@ -899,8 +782,6 @@ impl AcpThreadView { 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(); @@ -908,22 +789,6 @@ impl AcpThreadView { 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, tracked_buffers) = contents.await?; @@ -944,10 +809,6 @@ impl AcpThreadView { action_log.buffer_read(buffer, cx) } }); - drop(guard); - - telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); - thread.send(contents, cx) })?; send.await @@ -959,16 +820,6 @@ 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(); @@ -1012,24 +863,20 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; - if self.is_loading_contents { - return; - } - let Some(user_message_id) = thread.update(cx, |thread, _| { - thread.entries().get(entry_ix)?.user_message()?.id.clone() + 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)) }) else { return; }; - let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); + let contents = + message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); - 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) + let task = cx.foreground_executor().spawn(async move { + rewind.await?; + contents.await }); self.send_impl(task, window, cx); } @@ -1161,25 +1008,8 @@ 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 => { - 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 => {} + AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } @@ -1258,44 +1088,30 @@ impl AcpThreadView { 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; + 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.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(); - } - })); + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -1303,7 +1119,6 @@ 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 { @@ -1312,13 +1127,6 @@ 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(); } @@ -1340,10 +1148,6 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - let primary = match &entry { AgentThreadEntry::UserMessage(message) => { let Some(editor) = self @@ -1366,24 +1170,9 @@ 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)) - .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() - } - }) + .pt_2() .pb_4() .px_2() .gap_1p5() @@ -1392,7 +1181,6 @@ 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( @@ -1417,7 +1205,7 @@ impl AcpThreadView { div() .py_3() .px_2() - .rounded_md() + .rounded_lg() .shadow_md() .bg(cx.theme().colors().editor_background) .border_1() @@ -1435,89 +1223,47 @@ impl AcpThreadView { .text_xs() .child(editor.clone().into_any_element()), ) - .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() - }) - ) - ) - } - }), + .when(editing && 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, + ); + } + })), + ) + ) + ), ) .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - let pending_thinking_chunk_ix = if is_generating && is_last { - chunks - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| { - matches!(segment, AssistantMessageChunk::Thought { .. }) - }) - .map(|(index, _)| index) - } else { - None - }; - - let style = default_markdown_style(false, false, window, cx); + let style = default_markdown_style(false, window, cx); let message_body = v_flex() .w_full() .gap_2p5() @@ -1535,7 +1281,6 @@ impl AcpThreadView { entry_ix, chunk_ix, md.clone(), - Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1549,7 +1294,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(is_last, |this| this.pb_4()) + .when(entry_ix + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -1558,7 +1303,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().map(|this| { + div().w_full().py_1p5().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1577,14 +1322,17 @@ impl AcpThreadView { return primary; }; - let primary = if entry_ix == total_entries - 1 { + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + let primary = if entry_ix == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) - .child(self.render_thread_controls(&thread, cx)) + .child(self.render_thread_controls(cx)) .when_some( self.thread_feedback.comments_editor.clone(), - |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), + |this, editor| { + this.child(Self::render_feedback_feedback_editor(editor, window, cx)) + }, ) .into_any_element() } else { @@ -1634,90 +1382,65 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, - pending: bool, window: &Window, cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-card-header"); - let key = (entry_ix, chunk_ix); - let is_open = self.expanded_thinking_blocks.contains(&key); - let editor_bg = cx.theme().colors().editor_background; - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - let scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); v_flex() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) .child( h_flex() .id(header_id) .group(&card_header_id) .relative() .w_full() - .py_0p5() - .px_1p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .justify_between() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) + .gap_1p5() + .opacity(0.8) + .hover(|style| style.opacity(1.)) .child( h_flex() - .h(window.line_height()) - .gap_1p5() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) + .size_4() + .justify_center() .child( div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .map(|this| { - if pending { - this.child("Thinking") - } else { - this.child("Thought Process") - } - }), + .group_hover(&card_header_id, |s| s.invisible().w_0()) + .child( + Icon::new(IconName::ToolThink) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&card_header_id, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); + } + })), + ), ), ) .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({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); - } - cx.notify(); - } - })), + div() + .text_size(self.tool_name_font_size()) + .child("Thinking"), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -1730,40 +1453,33 @@ impl AcpThreadView { } })), ) - .child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .p_2() - .when(!is_open, |this| this.max_h_20()) - .text_ui_sm(cx) - .overflow_hidden() - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - .when(!is_open && pending, |this| this.child(gradient_overlay)), - ) + .when(is_open, |this| { + this.child( + div() + .relative() + .mt_1p5() + .ml(px(7.)) + .pl_4() + .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)), + ), + ) + }) .into_any_element() } - fn render_tool_call( + fn render_tool_call_icon( &self, + group_name: SharedString, entry_ix: usize, + is_collapsible: bool, + is_open: bool, tool_call: &ToolCall, - window: &Window, cx: &Context, ) -> Div { - let card_header_id = SharedString::from("inner-tool-call-header"); - let tool_icon = if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { FileIcons::get_icon(&tool_call.locations[0].path, cx) @@ -1771,7 +1487,7 @@ impl AcpThreadView { .unwrap_or(Icon::new(IconName::ToolPencil)) } else { Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolSearch, + acp::ToolKind::Read => IconName::ToolRead, acp::ToolKind::Edit => IconName::ToolPencil, acp::ToolKind::Delete => IconName::ToolDeleteFile, acp::ToolKind::Move => IconName::ArrowRightLeft, @@ -1785,12 +1501,77 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let failed_or_canceled = match &tool_call.status { - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, - _ => false, + 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, + tool_call: &ToolCall, + window: &Window, + cx: &Context, + ) -> Div { + 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 has_location = tool_call.locations.len() == 1; let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1803,31 +1584,23 @@ impl AcpThreadView { let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - let gradient_overlay = { + let gradient_overlay = |color: Hsla| { div() .absolute() .top_0() .right_0() .w_12() .h_full() - .map(|this| { - if use_card_layout { - this.bg(linear_gradient( - 90., - linear_color_stop(self.tool_card_header_bg(cx), 1.), - linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.), - )) - } else { - this.bg(linear_gradient( - 90., - linear_color_stop(cx.theme().colors().panel_background, 1.), - linear_color_stop( - cx.theme().colors().panel_background.opacity(0.2), - 0., - ), - )) - } - }) + .bg(linear_gradient( + 90., + linear_color_stop(color, 1.), + linear_color_stop(color.opacity(0.2), 0.), + )) + }; + let gradient_color = if use_card_layout { + self.tool_card_header_bg(cx) + } else { + cx.theme().colors().panel_background }; let tool_output_display = if is_open { @@ -1852,9 +1625,7 @@ impl AcpThreadView { .into_any() } ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit - && tool_call.content.is_empty() - && self.as_native_connection(cx).is_some() => + if is_edit && tool_call.content.is_empty() => { self.render_diff_loading(cx).into_any() } @@ -1878,58 +1649,50 @@ impl AcpThreadView { }; v_flex() - .map(|this| { - if use_card_layout { - this.my_2() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } + .when(use_card_layout, |this| { + this.rounded_lg() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() .child( h_flex() - .group(&card_header_id) - .relative() + .id(header_id) .w_full() .gap_1() .justify_between() - .when(use_card_layout, |this| { - this.p_0p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .when(is_open && !failed_or_canceled, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) + .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.)) + } }) .child( h_flex() + .group(&card_header_id) .relative() .w_full() - .h(window.line_height()) + .min_h_6() .text_size(self.tool_name_font_size()) - .gap_1p5() - .when(has_location || use_card_layout, |this| this.px_1()) - .when(has_location, |this| { - this.cursor(CursorStyle::PointingHand) - .rounded_sm() - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) - }) - .overflow_hidden() - .child(tool_icon) - .child(if has_location { + .child(self.render_tool_call_icon( + card_header_id, + entry_ix, + is_collapsible, + is_open, + tool_call, + cx, + )) + .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path .file_name() @@ -1940,12 +1703,17 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) .w_full() - .map(|this| { - if use_card_layout { - this.text_color(cx.theme().colors().text) - } else { - this.text_color(cx.theme().colors().text_muted) - } + .max_w_full() + .px_1p5() + .rounded_sm() + .overflow_x_scroll() + .opacity(0.8) + .hover(|label| { + label.opacity(1.).bg(cx + .theme() + .colors() + .element_hover + .opacity(0.5)) }) .child(name) .tooltip(Tooltip::text("Jump to File")) @@ -1955,48 +1723,38 @@ impl AcpThreadView { .into_any_element() } else { h_flex() + .id("non-card-label-container") .w_full() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, true, window, cx), - )) - .into_any() - }) - .when(!has_location, |this| this.child(gradient_overlay)), - ) - .when(is_collapsible || failed_or_canceled, |this| { - this.child( - h_flex() - .px_1() - .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), + .relative() + .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(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), ) .children(tool_output_display) } @@ -2037,19 +1795,22 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(rems(0.4)) + .ml(px(7.)) .px_3p5() .gap_2() .border_l_1() .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, false, window, cx))) + .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) .child( - IconButton::new(button_id, IconName::ChevronUp) + Button::new(button_id, "Collapse Output") .full_width() .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ChevronUp) .icon_color(Color::Muted) + .icon_position(IconPosition::Start) .on_click(cx.listener({ move |this: &mut Self, _, _, cx: &mut Context| { this.expanded_tool_calls.remove(&tool_call_id); @@ -2066,27 +1827,9 @@ 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(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() - } + let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { + path.to_string().into() } else { uri.clone() }; @@ -2094,7 +1837,7 @@ impl AcpThreadView { let button_id = SharedString::from(format!("item-{}", uri)); div() - .ml(rems(0.4)) + .ml(px(7.)) .pl_2p5() .border_l_1() .border_color(self.tool_card_border_color(cx)) @@ -2103,12 +1846,10 @@ 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| { @@ -2167,12 +1908,11 @@ impl AcpThreadView { let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, window, cx| { + move |this, _, _, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, - window, cx, ); } @@ -2241,7 +1981,7 @@ impl AcpThreadView { && diff.read(cx).has_revealed_range(cx) { editor.into_any_element() - } else if tool_progress && self.as_native_connection(cx).is_some() { + } else if tool_progress { self.render_diff_loading(cx) } else { Empty.into_any() @@ -2282,12 +2022,6 @@ impl AcpThreadView { started_at.elapsed() }; - let header_id = - SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id())); - let header_group = SharedString::from(format!( - "terminal-tool-header-group-{}", - terminal.entity_id() - )); let header_bg = cx .theme() .colors() @@ -2303,7 +2037,10 @@ impl AcpThreadView { let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() - .id(header_id) + .id(SharedString::from(format!( + "terminal-tool-header-{}", + terminal.entity_id() + ))) .flex_none() .gap_1() .justify_between() @@ -2367,6 +2104,23 @@ impl AcpThreadView { ), ) }) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", terminal.entity_id())) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when_some(output.and_then(|o| o.exit_status), |this, status| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + status.code().unwrap_or(-1), + ))) + }), + ) + }) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2375,7 +2129,7 @@ impl AcpThreadView { .to_string() } else { format!( - "Output is {} long, and to avoid unexpected token usage, \ + "Output is {} long—to avoid unexpected token usage, \ only 16 KB was sent back to the model.", format_file_size(output.original_content_len as u64, true), ) @@ -2419,7 +2173,6 @@ impl AcpThreadView { ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this, _event, _window, _cx| { @@ -2430,24 +2183,7 @@ impl AcpThreadView { } } })), - ) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", terminal.entity_id())) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when_some(output.and_then(|o| o.exit_status), |this, status| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - status.code().unwrap_or(-1), - ))) - }), - ) - }); + ); let terminal_view = self .entry_view_state @@ -2457,19 +2193,17 @@ impl AcpThreadView { let show_output = is_expanded && terminal_view.is_some(); v_flex() - .my_2() - .mx_5() + .mb_2() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) - .rounded_md() + .rounded_lg() .overflow_hidden() .child( v_flex() - .group(&header_group) .py_1p5() - .pr_1p5() .pl_2() + .pr_1p5() .gap_0p5() .bg(header_bg) .text_xs() @@ -2504,6 +2238,33 @@ 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)? @@ -2551,30 +2312,39 @@ impl AcpThreadView { return None; } - let has_both = user_rules_text.is_some() && rules_file_text.is_some(); - Some( - h_flex() + v_flex() .px_2p5() - .child( - Icon::new(IconName::Attach) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .gap_1() .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() + .group("user-rules") .id("user-rules") - .ml_1() - .mr_1p5() + .w_full() + .child( + Icon::new(IconName::Reader) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) .child( Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) - .truncate(), + .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")), ) - .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 { @@ -2585,25 +2355,33 @@ 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") - .ml_1p5() + .w_full() + .child( + Icon::new(IconName::Reader) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) .child( Label::new(rules_file_text) .size(LabelSize::XSmall) - .color(Color::Muted), + .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")), ) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .tooltip(Tooltip::text("View Project Rules")) .on_click(cx.listener(Self::handle_open_rules)), ) }) @@ -2635,7 +2413,8 @@ impl AcpThreadView { ) } - fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); let render_history = self .agent .clone() @@ -2647,10 +2426,42 @@ impl AcpThreadView { v_flex() .size_full() + .when(!render_history, |this| { + this.child( + 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() + }) + .child(h_flex().mt_4().mb_2().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() + })), + ) + }) .when(render_history, |this| { - let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { - history_store.entries().take(3).collect() - }); + let recent_history = self + .history_store + .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); this.justify_end().child( v_flex() .child( @@ -2721,276 +2532,239 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { - 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(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, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled { - error_message: _, - install_message: _, - install_command, - } => { - return self.render_not_installed(install_command.clone(), false, window, cx); - } - LoadError::Unsupported { - error_message: _, - upgrade_message: _, - upgrade_command, - } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); - } - 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 install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); - let task = self - .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(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_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() - } - - fn render_not_installed( - &self, - install_command: String, - is_upgrade: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - self.install_command_markdown.update(cx, |markdown, cx| { - if !markdown.source().contains(&install_command) { - markdown.replace(format!("```\n{}\n```", install_command), cx); - } - }); - - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; - v_flex() - .w_full() - .p_3p5() - .gap_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(linear_gradient( - 180., - linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.), - linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.), - )) + .p_2() + .gap_2() + .flex_1() + .items_center() + .justify_center() .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), + v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").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)), + ) + })) + .children( + configuration_view + .cloned() + .map(|view| div().px_4().w_full().max_w_128().child(view)), + ) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + div() + .text_ui(cx) + .text_center() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authentication required")), + ) + }, + ) + .when_some(pending_auth_method, |el, _| { + let spinner_icon = div() + .px_0p5() + .id("generating") + .tooltip(Tooltip::text("Generating Changes…")) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + ) + .into_any(); + el.child( + h_flex() + .text_ui(cx) + .text_center() + .justify_center() + .gap_2() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authenticating...")) + .child(spinner_icon), + ) + }) .child( - Button::new("install_gemini", button_label) - .full_width() - .size(ButtonSize::Medium) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .icon(IconName::TerminalGhost) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) + h_flex() + .mt_1p5() + .gap_1() + .flex_wrap() + .justify_center() + .children(connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .style(ButtonStyle::Outlined) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) + }) + .size(ButtonSize::Medium) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + 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| { - this.install_agent(install_command.clone(), 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() })), - ) - .child( - Label::new(or_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(MarkdownElement::new( - self.install_command_markdown.clone(), - default_markdown_style(false, false, window, cx), - )) - .into_any_element() + ); + } 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) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .size(ButtonSize::Medium) + .tooltip(Tooltip::text(install_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("install".to_string()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_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() + })), + ); + } + + container.into_any() } fn render_activity_bar( @@ -3203,13 +2977,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| { @@ -3481,19 +3255,6 @@ 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| { @@ -3569,7 +3330,6 @@ impl AcpThreadView { .child(self.render_send_button(cx)), ), ) - .when(!enable_editor, |this| this.child(backdrop)) .into_any() } @@ -3617,7 +3377,7 @@ impl AcpThreadView { "used-tokens-label", Animation::new(Duration::from_secs(2)) .repeat() - .with_easing(pulsating_between(0.3, 0.8)), + .with_easing(pulsating_between(0.6, 1.)), |label, delta| label.alpha(delta), ) .into_any() @@ -3716,14 +3476,7 @@ impl AcpThreadView { .thread() .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - 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 { + if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -3758,53 +3511,13 @@ 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.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()) - } - }; + let following = self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false); IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3813,10 +3526,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) + Tooltip::for_action("Stop Following Agent", &Follow, window, cx) } else { Tooltip::with_meta( - tooltip_label.clone(), + "Follow Agent", Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -3825,7 +3538,15 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_following(window, cx); + this.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); })) } @@ -3861,7 +3582,6 @@ 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| { @@ -3876,14 +3596,9 @@ impl AcpThreadView { }); } MentionUri::Symbol { - abs_path: path, - line_range, - .. + path, line_range, .. } - | MentionUri::Selection { - abs_path: Some(path), - line_range, - } => { + | MentionUri::Selection { path, line_range } => { let project = workspace.project(); let Some((path, _)) = project.update(cx, |project, cx| { let path = project.find_project_path(path, cx)?; @@ -3899,8 +3614,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( @@ -3915,7 +3630,6 @@ 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| { @@ -4118,8 +3832,7 @@ impl AcpThreadView { return; } - // TODO: Change this once we have title summarization for external agents. - let title = self.agent.name(); + let title = self.title(cx); match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { @@ -4237,21 +3950,7 @@ impl AcpThreadView { } } - 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").child( - div() - .py_2() - .px_5() - .child(SpinnerLabel::new().size(LabelSize::Small)), - ); - } - + fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4277,10 +3976,10 @@ impl AcpThreadView { .id("thread-controls-container") .group("thread-controls-container") .w_full() - .py_2() - .px_5() - .gap_px() - .opacity(0.6) + .mr_1() + .pb_2() + .px(RESPONSE_PADDING_X) + .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() .justify_end(); @@ -4291,57 +3990,68 @@ impl AcpThreadView { .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 { + 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( - 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); - })), - ); + 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() + fn render_feedback_feedback_editor( + editor: Entity, + window: &mut Window, + cx: &Context, + ) -> Div { + let focus_handle = editor.focus_handle(cx); + v_flex() .key_context("AgentFeedbackMessageEditor") .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { this.thread_feedback.dismiss_comments(); @@ -4350,31 +4060,43 @@ impl AcpThreadView { .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { this.submit_feedback_message(cx); })) - .p_2() .mb_2() - .mx_5() - .gap_1() + .mx_4() + .p_2() .rounded_md() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(editor)) + .child(editor) .child( h_flex() + .gap_1() + .justify_end() .child( - IconButton::new("dismiss-feedback-message", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + Button::new("dismiss-feedback-message", "Cancel") + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) .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) + Button::new("submit-feedback-message", "Share Feedback") + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) .on_click(cx.listener(move |this, _, _window, cx| { this.submit_feedback_message(cx); })), @@ -4615,53 +4337,11 @@ 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( - 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())), - ) + .actions_slot(self.create_copy_button(error.to_string())) .dismiss_action(self.dismiss_error_button(cx)) } @@ -4671,7 +4351,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) - .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( @@ -4691,7 +4370,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Authentication Required") - .icon(IconName::XCircle) .description(error.clone()) .actions_slot( h_flex() @@ -4717,7 +4395,6 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") - .icon(IconName::XCircle) .description(error_message) .actions_slot( h_flex() @@ -4849,24 +4526,6 @@ impl AcpThreadView { })) } - 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) @@ -4904,28 +4563,9 @@ impl AcpThreadView { } } -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 { - match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => { - self.message_editor.focus_handle(cx) - } - ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { - self.focus_handle.clone() - } - } + self.message_editor.focus_handle(cx) } } @@ -4941,7 +4581,6 @@ impl Render for AcpThreadView { .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 { @@ -4958,39 +4597,54 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(window, cx)), + ThreadState::Loading { .. } => { + v_flex().flex_1().child(self.render_empty_state(window, cx)) + } ThreadState::LoadError(e) => v_flex() + .p_2() .flex_1() - .size_full() .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)), - 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) - }), + .justify_center() + .child(self.render_load_error(e, cx)), + 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(), ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child(self.render_vertical_scrollbar(cx)) - } else { - this.child(self.render_recent_history(window, cx)) - } - }), + .child(self.render_vertical_scrollbar(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(), + }, + ) + } else { + this.child(self.render_empty_state(window, cx)) + } + }) + } }) // 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 @@ -5015,12 +4669,7 @@ impl Render for AcpThreadView { } } -fn default_markdown_style( - buffer_font: bool, - muted_text: bool, - window: &Window, - cx: &App, -) -> MarkdownStyle { +fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -5041,26 +4690,20 @@ fn default_markdown_style( 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(text_color), + color: Some(cx.theme().colors().text), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), - selection_background_color: colors.element_selection_background, + selection_background_color: cx.theme().colors().element_selection_background, code_block_overflow_x_scroll: true, table_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { @@ -5146,7 +4789,7 @@ fn plan_label_markdown_style( window: &Window, cx: &App, ) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, false, window, cx); + let default_md_style = default_markdown_style(false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -5166,7 +4809,7 @@ fn plan_label_markdown_style( } fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, false, window, cx); + let default_md_style = default_markdown_style(true, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -5407,24 +5050,20 @@ 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) -> SharedString { - "Test".into() + fn name(&self) -> &'static str { + "Test" } - fn empty_state_headline(&self) -> SharedString { - "Test".into() + fn empty_state_headline(&self) -> &'static str { + "Test" } - fn empty_state_message(&self) -> SharedString { - "Test".into() + fn empty_state_message(&self) -> &'static str { + "Test" } fn connect( @@ -5459,12 +5098,6 @@ pub(crate) mod tests { project, action_log, SessionId("test".into()), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }), - cx, ) }))) } @@ -5473,6 +5106,14 @@ pub(crate) mod tests { &[] } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2..2cad913295 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1595,6 +1595,11 @@ 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); diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e..00e48efdac 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,23 +3,19 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; -use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; -use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, + Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -27,24 +23,23 @@ 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, SettingsStore, update_settings_file}; +use settings::{Settings, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; +use workspace::Workspace; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, ExternalAgent, NewExternalAgentThread, + AddContextServer, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -52,7 +47,6 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -62,8 +56,6 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, - gemini_is_installed: bool, - _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -73,7 +65,6 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -98,34 +89,33 @@ 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 scrollbar_state = ScrollbarState::new(scroll_handle.clone()); + 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: HashMap::default(), + expanded_provider_configurations, tools, _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, - gemini_is_installed: false, - _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); - this.check_for_gemini(cx); this } @@ -155,34 +145,6 @@ 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 { @@ -257,6 +219,7 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) + .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -276,7 +239,10 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child(Label::new(provider_name.clone())) + .child( + Label::new(provider_name.clone()) + .size(LabelSize::Large), + ) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -321,7 +287,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Thread) + .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -420,7 +386,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") + Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), ), @@ -561,14 +527,6 @@ 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, @@ -586,12 +544,7 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child( - Label::new( - "All context servers connected through the Model Context Protocol.", - ) - .color(Color::Muted), - ), + .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -601,7 +554,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_1p5() + .gap_2() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -692,6 +645,8 @@ 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, @@ -834,8 +789,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(self.card_item_border_color(cx)) - .bg(self.card_item_bg_color(cx)) + .border_color(border_color) + .bg(cx.theme().colors().background.opacity(0.2)) .overflow_hidden() .child( h_flex() @@ -843,11 +798,7 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| { - element - .border_b_1() - .border_color(self.card_item_border_color(cx)) - }, + |element| element.border_b_1().border_color(border_color), ) .child( h_flex() @@ -1029,195 +980,6 @@ 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( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child(Headline::new("External Agents")) - .child( - Button::new("add-agent", "Add Agent") - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click( - move |_, window, cx| { - if let Some(workspace) = window.root().flatten() { - let workspace = workspace.downgrade(); - window - .spawn(cx, async |cx| { - open_new_agent_servers_entry_in_settings_editor( - workspace, - cx, - ).await - }) - .detach_and_log_err(cx); - } - } - ), - ) - ) - .child( - Label::new( - "Bring the agent of your choice to Zed via our new 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 { @@ -1237,7 +999,6 @@ 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)), ) @@ -1356,109 +1117,3 @@ fn show_unable_to_uninstall_extension_with_context_server( workspace.toggle_status_toast(status_toast, cx); } - -async fn open_new_agent_servers_entry_in_settings_editor( - workspace: WeakEntity, - cx: &mut AsyncWindowContext, -) -> Result<()> { - let settings_editor = workspace - .update_in(cx, |_, window, cx| { - create_and_open_local_file(paths::settings_file(), window, cx, || { - settings::initial_user_settings_content().as_ref().into() - }) - })? - .await? - .downcast::() - .unwrap(); - - settings_editor - .downgrade() - .update_in(cx, |item, window, cx| { - let text = item.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - - let mut unique_server_name = None; - let edits = settings.edits_for_update::(&text, |file| { - let server_name: Option = (0..u8::MAX) - .map(|i| { - if i == 0 { - "your_agent".into() - } else { - format!("your_agent_{}", i).into() - } - }) - .find(|name| !file.custom.contains_key(name)); - if let Some(server_name) = server_name { - unique_server_name = Some(server_name.clone()); - file.custom.insert( - server_name, - AgentServerSettings { - command: AgentServerCommand { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - }, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit(edits, cx); - if let Some((unique_server_name, buffer)) = - unique_server_name.zip(item.buffer().read(cx).as_singleton()) - { - let snapshot = buffer.read(cx).snapshot(); - if let Some(range) = - find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot) - { - item.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_ranges(vec![range]); - }, - ); - } - } - }) -} - -fn find_text_in_buffer( - text: &str, - start: usize, - snapshot: &language::BufferSnapshot, -) -> Option> { - let chars = text.chars().collect::>(); - - let mut offset = start; - let mut char_offset = 0; - for c in snapshot.chars_at(start) { - if char_offset >= chars.len() { - break; - } - offset += 1; - - if c == chars[char_offset] { - char_offset += 1; - } else { - char_offset = 0; - } - } - - if char_offset == chars.len() { - Some(offset.saturating_sub(chars.len())..offset) - } else { - None - } -} diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 1e1ff95178..e07424987c 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1529,7 +1529,6 @@ impl AgentDiff { | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Retry(_) => {} } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733..65a9da573a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,16 +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; -use crate::ui::AcpOnboardingModal; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -58,7 +54,9 @@ use gpui::{ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; -use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; +use language_model::{ + ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, +}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; @@ -78,10 +76,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{ - OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, - ToggleModelSelector, - }, + agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -135,7 +130,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.clone(), None, None, window, cx) + panel.external_thread(action.agent, None, None, window, cx) }); } }) @@ -205,9 +200,6 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) - .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { - AcpOnboardingModal::toggle(workspace, window, cx) - }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -249,8 +241,7 @@ enum WhichFontSize { None, } -// TODO unify this with ExternalAgent -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, @@ -258,29 +249,23 @@ pub enum AgentType { Gemini, ClaudeCode, NativeAgent, - Custom { - name: SharedString, - settings: AgentServerSettings, - }, } impl AgentType { - fn label(&self) -> SharedString { + fn label(self) -> impl Into { match self { - 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(), + Self::Zed | Self::TextThread => "Zed Agent", + Self::NativeAgent => "Agent 2", + Self::Gemini => "Gemini CLI", + Self::ClaudeCode => "Claude Code", } } - 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), } } } @@ -534,7 +519,7 @@ pub struct AgentPanel { impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent.clone(); + let selected_agent = self.selected_agent; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( @@ -598,6 +583,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( @@ -613,7 +609,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.clone(); + panel.selected_agent = selected_agent; panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); @@ -909,7 +905,7 @@ impl AgentPanel { fn active_thread_view(&self) -> Option<&Entity> { match &self.active_view { - ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view), + ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History @@ -1023,8 +1019,6 @@ impl AgentPanel { } 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)); @@ -1085,17 +1079,14 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { - 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(); - } + 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(); } }) .detach(); @@ -1117,15 +1108,11 @@ 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::Custom { .. } => { + crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { if !cx.has_flag::() { return; } @@ -1476,7 +1463,6 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), - self.project.downgrade(), window, cx, ) @@ -1848,8 +1834,21 @@ impl AgentPanel { menu } + pub fn set_selected_agent( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { + if self.selected_agent != agent { + self.selected_agent = agent; + self.serialize(cx); + } + self.new_agent_thread(agent, window, cx); + } + pub fn selected_agent(&self) -> AgentType { - self.selected_agent.clone() + self.selected_agent } pub fn new_agent_thread( @@ -1858,11 +1857,6 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - match agent { AgentType::Zed => { window.dispatch_action( @@ -1893,13 +1887,6 @@ impl AgentPanel { window, cx, ), - AgentType::Custom { name, settings } => self.external_thread( - Some(crate::ExternalAgent::Custom { name, settings }), - None, - None, - window, - cx, - ), } } @@ -2054,11 +2041,9 @@ impl AgentPanel { match state { 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() @@ -2090,33 +2075,9 @@ impl AgentPanel { } } ActiveView::ExternalAgentThread { thread_view } => { - 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() - } + Label::new(thread_view.read(cx).title(cx)) + .truncate() + .into_any_element() } ActiveView::TextThread { title_editor, @@ -2127,7 +2088,6 @@ impl AgentPanel { match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) - .color(Color::Muted) .truncate() .into_any_element(), ContextSummary::Content(summary) => { @@ -2139,7 +2099,6 @@ impl AgentPanel { } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() - .color(Color::Muted) .into_any_element() } } @@ -2200,8 +2159,6 @@ 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) @@ -2281,11 +2238,6 @@ 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 })) } @@ -2320,8 +2272,6 @@ 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| { @@ -2500,8 +2450,6 @@ impl AgentPanel { 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 @@ -2543,7 +2491,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::NativeAgent, window, cx, @@ -2569,7 +2517,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::TextThread, window, cx, @@ -2597,7 +2545,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::Gemini, window, cx, @@ -2624,7 +2572,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::ClaudeCode, window, cx, @@ -2636,64 +2584,13 @@ 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.new_agent_thread( - AgentType::Custom { - name: agent_name - .clone(), - settings: - agent_settings - .clone(), - }, - window, - cx, - ); - }); - } - }); - } - } - }), - ); - } - - menu - }) - .when(cx.has_flag::(), |menu| { - menu.separator().link( - "Add Other Agents", - OpenBrowser { - url: zed_urls::external_agents_docs(cx), - } - .boxed_clone(), - ) }); menu })) } }); - let selected_agent_label = self.selected_agent.label(); + let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") .when_some(self.selected_agent.icon(), |this, icon| { @@ -3278,6 +3175,17 @@ 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() + } } } @@ -3767,11 +3675,6 @@ 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 { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 110c432df3..6084fd6423 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,14 +28,13 @@ 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, SharedString, actions}; +use gpui::{Action, App, Entity, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, @@ -160,43 +159,25 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } -// TODO unify this with AgentType -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Copy, 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 { - 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, - )), + ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), + ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), } } } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 45e7529ec2..bed10e90a7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -378,13 +378,18 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = self + let Some(ConfiguredModel { model, provider }) = 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); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index e9e7eba4b6..9fbd90c4a6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -190,6 +190,7 @@ 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 @@ -288,6 +289,7 @@ 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| { @@ -361,12 +363,24 @@ 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 @@ -1916,6 +1930,7 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, + ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e..e27a224240 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,16 +1,12 @@ -mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; mod onboarding_modal; pub mod preview; -mod unavailable_editing_tooltip; -pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; -pub use unavailable_editing_tooltip::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs deleted file mode 100644 index 0ed9de7221..0000000000 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ /dev/null @@ -1,254 +0,0 @@ -use client::zed_urls; -use gpui::{ - ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, - linear_color_stop, linear_gradient, -}; -use ui::{TintColor, Vector, VectorName, prelude::*}; -use workspace::{ModalView, Workspace}; - -use crate::agent_panel::{AgentPanel, AgentType}; - -macro_rules! acp_onboarding_event { - ($name:expr) => { - telemetry::event!($name, source = "ACP Onboarding"); - }; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { - telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+); - }; -} - -pub struct AcpOnboardingModal { - focus_handle: FocusHandle, - workspace: Entity, -} - -impl AcpOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let workspace_entity = cx.entity(); - workspace.toggle_modal(window, cx, |_window, cx| Self { - workspace: workspace_entity, - focus_handle: cx.focus_handle(), - }); - } - - fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_agent_thread(AgentType::Gemini, window, cx); - }); - } - }); - - cx.emit(DismissEvent); - - acp_onboarding_event!("Open Panel Clicked"); - } - - fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { - cx.open_url(&zed_urls::external_agents_docs(cx)); - cx.notify(); - - acp_onboarding_event!("Documentation Link Clicked"); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter for AcpOnboardingModal {} - -impl Focusable for AcpOnboardingModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for AcpOnboardingModal {} - -impl Render for AcpOnboardingModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let illustration_element = |label: bool, opacity: f32| { - h_flex() - .px_1() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.05)) - .border_1() - .border_color(cx.theme().colors().border) - .border_dashed() - .child( - Icon::new(IconName::Stop) - .size(IconSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), - ) - .map(|this| { - if label { - this.child( - Label::new("Your Agent Here") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - div().w_16().h_1().rounded_full().bg(cx - .theme() - .colors() - .element_active - .opacity(0.6)), - ) - } - }) - .opacity(opacity) - }; - - let illustration = h_flex() - .relative() - .h(rems_from_px(126.)) - .bg(cx.theme().colors().editor_background) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_center() - .gap_8() - .rounded_t_md() - .overflow_hidden() - .child( - div().absolute().inset_0().w(px(515.)).h(px(126.)).child( - Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), - ), - ) - .child(div().absolute().inset_0().size_full().bg(linear_gradient( - 0., - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.1), - 0.9, - ), - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0., - ), - ))) - .child( - div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::black().opacity(0.15)), - ) - .child( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ), - ) - .child( - v_flex() - .gap_1p5() - .child(illustration_element(false, 0.15)) - .child(illustration_element(true, 0.3)) - .child( - h_flex() - .pl_1() - .pr_2() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.2)) - .border_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::AiGemini) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)), - ) - .child(illustration_element(true, 0.3)) - .child(illustration_element(false, 0.15)), - ); - - let heading = v_flex() - .w_full() - .gap_1() - .child( - Label::new("Now Available") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large)); - - let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration."; - - let open_panel_button = Button::new("open-panel", "Start with Gemini CLI") - .icon_size(IconSize::Indicator) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::open_panel)); - - let docs_button = Button::new("add-other-agents", "Add Other Agents") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .full_width() - .on_click(cx.listener(Self::view_docs)); - - let close_button = h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( - |_, _: &ClickEvent, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "X click"); - cx.emit(DismissEvent); - }, - )), - ); - - v_flex() - .id("acp-onboarding") - .key_context("AcpOnboardingModal") - .relative() - .w(rems(34.)) - .h_full() - .elevation_3(cx) - .track_focus(&self.focus_handle(cx)) - .overflow_hidden() - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "Action"); - cx.emit(DismissEvent); - })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { - this.focus_handle.focus(window); - })) - .child(illustration) - .child( - v_flex() - .p_4() - .gap_2() - .child(heading) - .child(Label::new(copy).color(Color::Muted)) - .child( - v_flex() - .w_full() - .mt_2() - .gap_1() - .child(open_panel_button) - .child(docs_button), - ), - ) - .child(close_button) - } -} diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index d4d037b976..29b12ea627 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -86,18 +86,23 @@ impl RenderOnce for UsageCallout { (IconName::Warning, Severity::Warning) }; - 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); - }), + 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); + }), + ), ) .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 deleted file mode 100644 index 78d4c64e0a..0000000000 --- a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs +++ /dev/null @@ -1,29 +0,0 @@ -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/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 6d8ac64725..717abebfd1 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, Tooltip, prelude::*}; +use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -43,10 +43,12 @@ 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>, } @@ -62,9 +64,17 @@ 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(); @@ -84,6 +94,42 @@ 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; @@ -313,10 +359,14 @@ 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) { - 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), + 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() } } else { self.render_sign_in_disclaimer(cx) @@ -340,15 +390,18 @@ 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() @@ -362,23 +415,27 @@ impl Component for ZedAiOnboarding { .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, None, false), + onboarding(SignInStatus::SignedOut, false, None, false), + ), + single_example( + "Not Accepted ToS", + onboarding(SignInStatus::SignedIn, false, None, false), ), single_example( "Young Account", - onboarding(SignInStatus::SignedIn, None, true), + onboarding(SignInStatus::SignedIn, true, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), + onboarding(SignInStatus::SignedIn, true, 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 106dcb0aef..e9639ca075 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 { - sign_in_status: SignInStatus, - sign_in: Arc, - account_too_young: bool, - user_plan: Option, - tab_index: Option, + pub sign_in_status: SignInStatus, + pub sign_in: Arc, + pub account_too_young: bool, + pub user_plan: Option, + pub tab_index: Option, } impl AiUpsellCard { @@ -43,11 +43,6 @@ 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_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index cc22c9fc09..79e205f205 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 { - true + false } 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 d1451132ae..ac2c7a32ab 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -435,8 +435,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); @@ -447,8 +447,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); } diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index a6e984fca6..766ee3b161 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::ToolSearch + IconName::ToolRead } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index b20dad4ebb..10b59d0ba2 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -162,22 +162,6 @@ 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>, @@ -229,10 +213,7 @@ impl BufferDiffSnapshot { cx: &App, ) -> impl Future + use<> { let base_text_exists = base_text.is_some(); - let base_text_pair = base_text.map(|text| { - debug_assert_eq!(&*text, &base_text_snapshot.text()); - (text, base_text_snapshot.as_rope().clone()) - }); + let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { @@ -892,18 +873,6 @@ 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, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 2bbe7dd1b5..ed3f114943 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,8 +66,6 @@ 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() @@ -1292,21 +1290,19 @@ 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 = 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?), - }) + 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?), } - })? - .await?; + }; log::info!("connected to rpc endpoint {}", rpc_url); @@ -1394,13 +1390,11 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - if !*USE_WEB_LOGIN { - eprintln!("authenticate as admin {login}, {token}"); + 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. diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a5c1532c75..f3142a0af6 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(str::to_string) + .map(|s| s.to_owned()) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d23eb37519..20f99e3944 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}; +use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; @@ -46,6 +46,11 @@ impl ProjectId { } } +#[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); @@ -111,6 +116,7 @@ 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>, @@ -188,6 +194,7 @@ 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(), @@ -264,6 +271,7 @@ 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() @@ -783,6 +791,19 @@ 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, @@ -825,6 +846,32 @@ 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/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 7193c09947..9df41906d7 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String { server_url = server_url(cx) ) } - -/// Returns the URL to Zed AI's external agents documentation. -pub fn external_agents_docs(cx: &App) -> String { - format!( - "{server_url}/docs/ai/external-agents", - server_url = server_url(cx) - ) -} diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 7fd96fcef0..92417d8319 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -102,7 +102,13 @@ impl CloudApiClient { let credentials = credentials.as_ref().context("no credentials provided")?; let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); - Ok(Tokio::spawn_result(cx, async move { + 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(); + let ws = WebSocket::connect(connect_url) .with_request( request::Builder::new() @@ -115,6 +121,34 @@ 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/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1e0c915bcb..d9fd8ffeb2 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_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, 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_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, 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_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, 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_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d85a6610a5..cd37549783 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2905,8 +2905,6 @@ 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( @@ -2922,7 +2920,8 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.join_channel_chat(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel chat")), + .tooltip(Tooltip::text("Open channel chat")) + .visible_on_hover(""), ) .child( IconButton::new("channel_notes", IconName::Reader) @@ -2937,9 +2936,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({ diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 01cf403083..5be97c36bc 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,10 +1,7 @@ use anyhow::Result; use db::{ - query, - sqlez::{ - bindable::Column, domain::Domain, statement::Statement, - thread_safe_connection::ThreadSafeConnection, - }, + define_connection, query, + sqlez::{bindable::Column, statement::Statement}, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation { } } -pub struct CommandPaletteDB(ThreadSafeConnection); - -impl Domain for CommandPaletteDB { - const NAME: &str = stringify!(CommandPaletteDB); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = + &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB { last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -} - -db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); +); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/context_server/src/test.rs b/crates/context_server/src/test.rs index 008542ab24..dedf589664 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::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex}; +use futures::{Stream, StreamExt as _, lock::Mutex}; use gpui::BackgroundExecutor; use std::{pin::Pin, sync::Arc}; @@ -14,12 +14,9 @@ pub fn create_fake_transport( executor: BackgroundExecutor, ) -> FakeTransport { let name = name.into(); - FakeTransport::new(executor).on_request::( - move |_params| { - let name = name.clone(); - async move { create_initialize_response(name.clone()) } - }, - ) + FakeTransport::new(executor).on_request::(move |_params| { + create_initialize_response(name.clone()) + }) } fn create_initialize_response(server_name: String) -> InitializeResponse { @@ -35,10 +32,8 @@ fn create_initialize_response(server_name: String) -> InitializeResponse { } pub struct FakeTransport { - request_handlers: HashMap< - &'static str, - Arc BoxFuture<'static, serde_json::Value>>, - >, + request_handlers: + HashMap<&'static str, Arc serde_json::Value + Send + Sync>>, tx: futures::channel::mpsc::UnboundedSender, rx: Arc>>, executor: BackgroundExecutor, @@ -55,25 +50,18 @@ impl FakeTransport { } } - pub fn on_request( + pub fn on_request( mut self, - handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut, - ) -> Self - where - T: crate::types::Request, - Fut: 'static + Send + Future, - { + handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static, + ) -> Self { self.request_handlers.insert( T::METHOD, Arc::new(move |value| { - let params = value - .get("params") - .cloned() - .unwrap_or(serde_json::Value::Null); + let params = value.get("params").expect("Missing parameters").clone(); let params: T::Params = serde_json::from_value(params).expect("Invalid parameters received"); let response = handler(params); - async move { serde_json::to_value(response.await).unwrap() }.boxed() + serde_json::to_value(response).unwrap() }), ); self @@ -89,7 +77,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).await; + let payload = handler(msg); let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 52d75175e5..9308500ed4 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -301,7 +301,6 @@ 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, @@ -534,7 +533,6 @@ 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, diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index 370f0bb5f6..f12913d1cb 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -6,7 +6,6 @@ edition.workspace = true license = "GPL-3.0-or-later" [dependencies] -bincode.workspace = true crash-handler.workspace = true log.workspace = true minidumper.workspace = true @@ -15,7 +14,6 @@ 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 f7bc96bff9..b1afc5ae45 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -127,7 +127,6 @@ unsafe fn suspend_all_other_threads() { pub struct CrashServer { initialization_params: OnceLock, panic_info: OnceLock, - active_gpu: OnceLock, has_connection: Arc, } @@ -136,8 +135,6 @@ 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)] @@ -146,6 +143,7 @@ pub struct InitCrashHandler { pub zed_version: String, pub release_channel: String, pub commit_sha: String, + // pub gpu: String, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -180,18 +178,6 @@ 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 @@ -200,8 +186,6 @@ 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() @@ -227,13 +211,6 @@ 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"); } @@ -310,7 +287,6 @@ 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/db/src/db.rs b/crates/db/src/db.rs index 0802bd8bb7..8b790cbec8 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,14 +110,11 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain -/// -/// Arguments: -/// - static variable name for connection -/// - type of connection wrapper -/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! static_connection { - ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { +macro_rules! define_connection { + (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -126,6 +123,16 @@ macro_rules! static_connection { } } + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -135,8 +142,7 @@ macro_rules! static_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) + $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -147,10 +153,46 @@ macro_rules! static_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) + $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) }); - } + }; + (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl ::std::ops::Deref for $t { + type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + + #[cfg(any(test, feature = "test-support"))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) + }); + + #[cfg(not(any(test, feature = "test-support")))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) + }); + }; } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -177,12 +219,17 @@ mod tests { enum BadDB {} impl Domain for BadDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ] + } } let tempdir = tempfile::Builder::new() @@ -204,15 +251,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() @@ -248,16 +305,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; + fn name() -> &'static str { + "db_tests" + } - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 8ea877b35b..256b789c9b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,26 +2,16 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - write_and_log, -}; +use crate::{define_connection, query, write_and_log}; -pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for KeyValueStore { - const NAME: &str = stringify!(KeyValueStore); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); +); pub trait Dismissable { const KEY: &'static str; @@ -101,19 +91,15 @@ mod tests { } } -pub struct GlobalKeyValueStore(ThreadSafeConnection); - -impl Domain for GlobalKeyValueStore { - const NAME: &str = stringify!(GlobalKeyValueStore); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); + global +); impl GlobalKeyValueStore { query! { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 9991395f35..0574091851 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -916,10 +916,7 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let ssh_info = project - .read(cx) - .ssh_client() - .and_then(|it| it.read(cx).ssh_info()); + let is_local = project.read(cx).is_local(); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1003,7 +1000,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); + let builder = ShellBuilder::new(is_local, &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); diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index fd678078e8..53b5792e10 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,6 +18,7 @@ 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 1c27e820a0..2e20118381 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,6 +13,7 @@ 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, @@ -23,6 +24,7 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, + lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -77,10 +79,17 @@ 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); @@ -251,7 +260,11 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(false, window, cx); + }) + .detach(); + cx.observe_release(&cx.entity(), |editor, _, cx| { + editor.stop_cargo_diagnostics_fetch(cx); }) .detach(); @@ -268,10 +281,15 @@ 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_excerpts(window, cx); + this.update_all_diagnostics(true, window, cx); this } @@ -355,10 +373,20 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - if self.update_excerpts_task.is_some() { + 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() { self.update_excerpts_task = None; } else { - self.update_all_excerpts(window, cx); + self.update_all_diagnostics(false, window, cx); } cx.notify(); } @@ -376,6 +404,73 @@ 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) { @@ -600,6 +695,30 @@ 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/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 404db39164..e77b80115f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -13,18 +15,26 @@ 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 = diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some(); + 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() + }; } let tooltip = if include_warnings { @@ -54,6 +64,7 @@ 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(); }); @@ -65,7 +76,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts) + .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -73,8 +84,17 @@ 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| { - diagnostics.update_all_excerpts(window, cx); + if fetch_cargo_diagnostics { + diagnostics.fetch_cargo_diagnostics( + cargo_diagnostics_sources, + cx, + ); + } else { + diagnostics.update_all_excerpts(window, cx); + } }); } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8c3dc54b7..c900eb692a 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); -static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { - load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") -}); - static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); const FRONT_MATTER_COMMENT: &str = ""; @@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, - "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 6b695af1ae..964f202934 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -89,6 +89,9 @@ 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, @@ -121,6 +124,7 @@ 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, @@ -192,6 +196,10 @@ 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 0e3fe8cb1a..4f69af7ee4 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -242,9 +242,13 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal() { + if zeta::should_show_upsell_modal(&self.user_store, cx) { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - "Choose a Plan" + if self.user_store.read(cx).has_accepted_terms_of_service() { + "Choose a Plan" + } else { + "Accept the Terms of Service" + } } else { "Sign In" }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c0..2af8e6c0e4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -253,6 +253,7 @@ pub type RenderDiffHunkControlsFn = Arc< enum ReportEditorEvent { Saved { auto_saved: bool }, EditorOpened, + ZetaTosClicked, Closed, } @@ -261,6 +262,7 @@ impl ReportEditorEvent { match self { Self::Saved { .. } => "Editor Saved", Self::EditorOpened => "Editor Opened", + Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", Self::Closed => "Editor Closed", } } @@ -2588,7 +2590,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -5574,11 +5576,6 @@ 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( @@ -5590,11 +5587,9 @@ impl Editor { cx, ); - let words = match (omit_word_completions, completion_settings.words) { - (true, _) | (_, WordsCompletionMode::Disabled) => { - Task::ready(BTreeMap::default()) - } - (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx + let words = match completion_settings.words { + WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), + WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5606,20 +5601,16 @@ impl Editor { (words, provider_responses) } - 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, - }) + None => ( + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, }) - }; - (words, Task::ready(Ok(Vec::new()))) - } + }), + Task::ready(Ok(Vec::new())), + ), }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -7686,16 +7677,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); } if modifiers_held { @@ -9044,7 +9035,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +9047,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(&accept_keystroke.key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9171,13 +9162,52 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::KeybindingKeystroke>, + accept_keystroke: Option<&gpui::Keystroke>, _window: &Window, cx: &mut Context, ) -> Option { 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 { @@ -9249,7 +9279,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9349,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers.modified() { return el; } @@ -9338,7 +9368,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted @@ -9779,9 +9809,6 @@ 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); @@ -9875,9 +9902,6 @@ 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| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2cfdb92593..96261fdb2c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,9 +57,7 @@ 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] @@ -12239,7 +12237,6 @@ 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, }); @@ -12298,7 +12295,6 @@ 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, @@ -12335,7 +12331,6 @@ 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, @@ -13077,7 +13072,6 @@ 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, @@ -13174,7 +13168,6 @@ 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, @@ -13238,7 +13231,6 @@ 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, @@ -13312,7 +13304,6 @@ 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, @@ -13370,56 +13361,6 @@ 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, @@ -22715,7 +22656,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24302,7 +24243,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(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }) }) .unwrap(); @@ -24350,41 +24291,6 @@ 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 91034829f7..797b0d6634 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,10 +43,10 @@ use gpui::{ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -74,7 +74,6 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -90,8 +89,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, - item::Item, notifications::NotifyTaskExt, + CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, + notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -3603,187 +3602,171 @@ 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, - 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, + 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, - ); - }); - } else { - // Regular click toggles single buffer - if is_folded { + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - 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), - ) - .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 + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); } else { - Color::Created - }) - .when(status.is_deleted(), |el| { - el.strikethrough() - }) + 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 }, )) - }) - .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, - ); - } - })), - ), - ); + }), + ) + .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(); @@ -3799,31 +3782,25 @@ 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.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 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 parent_abs_path = abs_path - .as_ref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = + abs_path.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.is_visible(); + relative_path.is_some() && worktree.read(cx).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, |menu, abs_path| { + .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), @@ -7150,7 +7127,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { + pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd..afc5767de0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,7 +42,6 @@ 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}, }; @@ -1402,16 +1401,6 @@ impl ProjectItem for Editor { editor } - - fn for_broken_project_item( - abs_path: &Path, - 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/persistence.rs b/crates/editor/src/persistence.rs index ec7c149b4e..88fde53947 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use db::{ - query, - sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - domain::Domain, - statement::Statement, - }, - sqlez_macros::sql, -}; +use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; +use db::sqlez::statement::Statement; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; +use db::sqlez_macros::sql; +use db::{define_connection, query}; + use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -87,11 +83,7 @@ impl Column for SerializedEditor { } } -pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for EditorDb { - const NAME: &str = stringify!(EditorDb); - +define_connection!( // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -121,8 +113,7 @@ impl Domain for EditorDb { // start: usize, // end: usize, // ) - - const MIGRATIONS: &[&str] = &[ + pub static ref DB: EditorDb = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -198,9 +189,7 @@ impl Domain for EditorDb { ) STRICT; ), ]; -} - -db::static_connection!(DB, EditorDb, [WorkspaceDb]); +); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index cf74ee0a9e..e3d83ab160 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,17 +26,6 @@ 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() @@ -49,6 +38,9 @@ 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); } } @@ -317,7 +309,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -329,7 +321,10 @@ 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); } @@ -342,7 +337,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -354,7 +349,10 @@ 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); } @@ -367,7 +365,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -379,6 +377,9 @@ 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/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b3..422979c429 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; - - fn enabled_for_all() -> bool { - true - } } pub struct ClaudeCodeFeatureFlag; @@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(T::enabled_for_all()) + .unwrap_or(false) } fn is_staff(&self) -> bool { diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index db872f7a15..3a2c1fd713 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,9 +15,13 @@ path = "src/feedback.rs" test-support = [] [dependencies] +client.workspace = true gpui.workspace = true +human_bytes = "0.4.1" menu.workspace = true -system_specs.workspace = true +release_channel.workspace = true +serde.workspace = true +sysinfo.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 3822dd7ba3..40c2707d34 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,14 +1,18 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; +use system_specs::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/system_specs/src/system_specs.rs b/crates/feedback/src/system_specs.rs similarity index 59% rename from crates/system_specs/src/system_specs.rs rename to crates/feedback/src/system_specs.rs index 731d335232..87642ab929 100644 --- a/crates/system_specs/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -1,22 +1,11 @@ -//! # system_specs - use client::telemetry; -pub use gpui::GpuSpecs; -use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions}; +use gpui::{App, AppContext as _, SemanticVersion, Task, Window}; use human_bytes::human_bytes; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; -actions!( - zed, - [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, - ] -); - #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { app_version: String, @@ -169,115 +158,6 @@ fn try_determine_available_gpus() -> Option { } } -#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] -pub struct GpuInfo { - pub device_name: Option, - pub device_pci_id: u16, - pub vendor_name: Option, - pub vendor_pci_id: u16, - pub driver_version: Option, - pub driver_name: Option, -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result> { - use anyhow::Context as _; - use pciid_parser; - let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?; - let mut pci_addresses = vec![]; - let mut gpus = Vec::::new(); - let pci_db = pciid_parser::Database::read().ok(); - for entry in dir_iter { - let Ok(entry) = entry else { - continue; - }; - - let device_path = entry.path().join("device"); - let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| { - pci_address - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }) else { - continue; - }; - let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else { - continue; - }; - let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else { - continue; - }; - let driver_name = std::fs::read_link(device_path.join("driver")) - .ok() - .and_then(|driver_link| { - driver_link - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }); - let driver_version = driver_name - .as_ref() - .and_then(|driver_name| { - std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok() - }) - .as_deref() - .map(str::trim) - .map(str::to_string); - - let already_found = gpus - .iter() - .zip(&pci_addresses) - .any(|(gpu, gpu_pci_address)| { - gpu_pci_address == &pci_address - && gpu.driver_version == driver_version - && gpu.driver_name == driver_name - }); - - if already_found { - continue; - } - - let vendor = pci_db - .as_ref() - .and_then(|db| db.vendors.get(&vendor_pci_id)); - let vendor_name = vendor.map(|vendor| vendor.name.clone()); - let device_name = vendor - .and_then(|vendor| vendor.devices.get(&device_pci_id)) - .map(|device| device.name.clone()); - - gpus.push(GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - }); - pci_addresses.push(pci_address); - } - - Ok(gpus) -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn read_pci_id_from_path(path: impl AsRef) -> anyhow::Result { - use anyhow::Context as _; - let id = std::fs::read_to_string(path)?; - let id = id - .trim() - .strip_prefix("0x") - .context("Not a device ID") - .context(id.clone())?; - anyhow::ensure!( - id.len() == 4, - "Not a device id, expected 4 digits, found {}", - id.len() - ); - u16::from_str_radix(id, 16).context("Failed to parse device ID") -} - /// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime. /// /// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 7512152324..8aaaa04729 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1401,16 +1401,13 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim(); + let raw_query = raw_query.trim().to_owned(); - 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 { + let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path.unwrap().len()) + Some(path_position.path.to_str().unwrap().len()) }; let query = FileSearchQuery { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index cd0f203d6a..8203d1b1fd 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,7 +218,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", - "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -253,53 +252,6 @@ 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/gpui/src/app.rs b/crates/gpui/src/app.rs index b59d7e717a..bbd59fa7bc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -37,10 +37,10 @@ use crate::{ AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, - PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, - Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, - Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, + PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, + SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, + WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -263,7 +263,6 @@ pub struct App { pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -313,7 +312,6 @@ impl App { let text_system = Arc::new(TextSystem::new(platform.text_system())); let entities = EntityMap::new(); let keyboard_layout = platform.keyboard_layout(); - let keyboard_mapper = platform.keyboard_mapper(); let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(App { @@ -339,7 +337,6 @@ impl App { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), keymap: Rc::new(RefCell::new(Keymap::default())), keyboard_layout, - keyboard_mapper, global_action_listeners: FxHashMap::default(), pending_effects: VecDeque::new(), pending_notifications: FxHashSet::default(), @@ -379,7 +376,6 @@ impl App { if let Some(app) = app.upgrade() { let cx = &mut app.borrow_mut(); cx.keyboard_layout = cx.platform.keyboard_layout(); - cx.keyboard_mapper = cx.platform.keyboard_mapper(); cx.keyboard_layout_observers .clone() .retain(&(), move |callback| (callback)(cx)); @@ -428,11 +424,6 @@ impl App { self.keyboard_layout.as_ref() } - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 0f5b98df39..5e4b5fe6e9 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -352,7 +352,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Default, Debug)] 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 b3db09d821..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use crate::{Action, Keystroke, is_no_action}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -141,7 +141,7 @@ impl Keymap { /// only. pub fn bindings_for_input( &self, - input: &[impl AsKeystroke], + input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); @@ -192,6 +192,7 @@ impl Keymap { (bindings, !pending.is_empty()) } + /// Check if the given binding is enabled, given a certain key context. /// Returns the deepest depth at which the binding matches, or None if it doesn't match. fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { @@ -638,7 +639,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner.unparse()) + .map(|binding| binding.keystrokes[0].unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c54..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,15 +1,14 @@ use std::rc::Rc; -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; +use collections::HashMap; + +use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -33,15 +32,7 @@ impl KeyBinding { pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context_predicate = context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() + Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } /// Load a keybinding from the given raw data. @@ -49,22 +40,24 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) + .map(Keystroke::parse) .collect::>()?; + if let Some(equivalents) = key_equivalents { + for keystroke in keystrokes.iter_mut() { + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + } + Ok(Self { keystrokes, action, @@ -86,13 +79,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { + if !typed.should_match(target) { return None; } } @@ -101,7 +94,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc56..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static { fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); + fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..e28d781520 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,7 +1,3 @@ -use collections::HashMap; - -use crate::{KeybindingKeystroke, Keystroke}; - /// A trait for platform-specific keyboard layouts pub trait PlatformKeyboardLayout { /// Get the keyboard layout ID, which should be unique to the layout @@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout { /// Get the keyboard layout display name fn name(&self) -> &str; } - -/// A trait for platform-specific keyboard mappings -pub trait PlatformKeyboardMapper { - /// Map a key equivalent to its platform-specific representation - fn map_key_equivalent( - &self, - keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke; - /// Get the key equivalents for the current keyboard layout, - /// only used on macOS - fn get_key_equivalents(&self) -> Option<&HashMap>; -} - -/// A dummy implementation of the platform keyboard mapper -pub struct DummyKeyboardMapper; - -impl PlatformKeyboardMapper for DummyKeyboardMapper { - fn map_key_equivalent( - &self, - keystroke: Keystroke, - _use_key_equivalents: bool, - ) -> KeybindingKeystroke { - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,14 +5,6 @@ use std::{ fmt::{Display, Write}, }; -use crate::PlatformKeyboardMapper; - -/// This is a helper trait so that we can simplify the implementation of some functions -pub trait AsKeystroke { - /// Returns the GPUI representation of the keystroke. - fn as_keystroke(&self) -> &Keystroke; -} - /// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -32,17 +24,6 @@ pub struct Keystroke { pub key_char: Option, } -/// Represents a keystroke that can be used in keybindings and displayed to the user. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct KeybindingKeystroke { - /// The GPUI representation of the keystroke. - pub inner: Keystroke, - /// The modifiers to display. - pub display_modifiers: Modifiers, - /// The key to display. - pub display_key: String, -} - /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] @@ -77,7 +58,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -90,7 +71,7 @@ impl Keystroke { ..Default::default() }; - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -102,12 +83,12 @@ impl Keystroke { .filter(|key_char| key_char != &&self.key) { // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + if &target.key == key_char && target.modifiers == Modifiers::none() { return true; } } - target.inner.modifiers == self.modifiers && target.inner.key == self.key + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: @@ -219,7 +200,31 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) + let mut str = String::new(); + if self.modifiers.function { + str.push_str("fn-"); + } + if self.modifiers.control { + str.push_str("ctrl-"); + } + if self.modifiers.alt { + str.push_str("alt-"); + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + str.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + str.push_str("super-"); + + #[cfg(target_os = "windows")] + str.push_str("win-"); + } + if self.modifiers.shift { + str.push_str("shift-"); + } + str.push_str(&self.key); + str } /// Returns true if this keystroke left @@ -261,32 +266,6 @@ impl Keystroke { } } -impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) - } -} - fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} + if self.modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if self.modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if self.modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + let key = match self.key.as_str() { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) } } @@ -571,110 +600,3 @@ pub struct Capslock { #[serde(default)] pub on: bool, } - -impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } -} - -impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } -} - -fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - Ok(()) -} - -fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = match key { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) -} - -#[inline] -fn unparse(modifiers: &Modifiers, key: &str) -> String { - let mut result = String::new(); - if modifiers.function { - result.push_str("fn-"); - } - if modifiers.control { - result.push_str("ctrl-"); - } - if modifiers.alt { - result.push_str("alt-"); - } - if modifiers.platform { - #[cfg(target_os = "macos")] - result.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - result.push_str("super-"); - - #[cfg(target_os = "windows")] - result.push_str("win-"); - } - if modifiers.shift { - result.push_str("shift-"); - } - result.push_str(&key); - result -} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, - Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, + Point, Result, Task, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -144,10 +144,6 @@ impl Platform for P { self.keyboard_layout() } - fn keyboard_mapper(&self) -> Rc { - Rc::new(crate::DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index 1409731246..a9f6af3edb 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,9 +1,8 @@ -use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; +use crate::PlatformKeyboardLayout; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -15,10 +14,6 @@ pub(crate) struct MacKeyboardLayout { name: String, } -pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, -} - impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -29,27 +24,6 @@ impl PlatformKeyboardLayout for MacKeyboardLayout { } } -impl PlatformKeyboardMapper for MacKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { - if keystroke.key.chars().count() == 1 - && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - self.key_equivalents.as_ref() - } -} - impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -73,1428 +47,3 @@ impl MacKeyboardLayout { } } } - -impl MacKeyboardMapper { - pub(crate) fn new(layout_id: &str) -> Self { - let key_equivalents = get_key_equivalents(layout_id); - - Self { key_equivalents } - } -} - -// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range -// without using option. This means that some of our built in keyboard shortcuts do not work -// for those users. -// -// The way macOS solves this problem is to move shortcuts around so that they are all reachable, -// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct -// -// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. -// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves -// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position -// as cmd-> on a QWERTY layout. -// -// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö -// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard -// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the -// specific key moves) -// -// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every -// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... -// -// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the -// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: -// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' -// From there I used multi-cursor to produce this match statement. -fn get_key_equivalents(layout_id: &str) -> Option> { - let mappings: &[(char, char)] = match layout_id { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], - - _ => return None, - }; - - Some(HashMap::from_iter(mappings.iter().cloned())) -} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00..832550dc46 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, MacKeyboardMapper, + BoolExt, MacKeyboardLayout, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -8,9 +8,8 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, - hash, + PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, - keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -191,9 +189,6 @@ impl MacPlatform { #[cfg(not(feature = "font-kit"))] let text_system = Arc::new(crate::NoopTextSystem::new()); - let keyboard_layout = MacKeyboardLayout::new(); - let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { headless, text_system, @@ -214,7 +209,6 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, - keyboard_mapper, })) } @@ -354,19 +348,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -379,7 +373,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -888,10 +882,6 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; let mut lock = platform.0.lock(); - let keyboard_layout = MacKeyboardLayout::new(); - lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); if let Some(mut callback) = lock.on_keyboard_layout_change.take() { drop(lock); callback(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199f..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,8 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, - ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -238,10 +237,6 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index f554dea128..e5b9c020d5 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,8 +9,10 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ + Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, + WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -54,7 +56,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); + ThreadPool::RunWithPriorityAndOptionsAsync( + &handler, + WorkItemPriority::High, + WorkItemOptions::TimeSliced, + ) + .log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -65,7 +72,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + 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(); } } diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c..371feb70c2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,31 +1,22 @@ use anyhow::Result; -use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, - VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, - VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, - VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, + VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, + VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, + VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; +use crate::{Modifiers, PlatformKeyboardLayout}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout { } } -impl PlatformKeyboardMapper for WindowsKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) - else { - return KeybindingKeystroke::from_keystroke(keystroke); - }; - if shifted_key && keystroke.modifiers.shift { - log::warn!( - "Keystroke '{}' has both shift and a shifted key, this is likely a bug", - keystroke.key - ); - } - - let shift = shifted_key || keystroke.modifiers.shift; - keystroke.modifiers.shift = false; - - let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { - log::error!( - "Failed to map key equivalent '{:?}' to a valid key", - keystroke - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - - keystroke.key = if shift { - let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { - log::error!( - "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", - keystroke, - vkey - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - shifted_key - } else { - key.clone() - }; - - let modifiers = Modifiers { - shift, - ..keystroke.modifiers - }; - - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -116,41 +48,6 @@ impl WindowsKeyboardLayout { } } -impl WindowsKeyboardMapper { - pub(crate) fn new() -> Self { - let mut key_to_vkey = HashMap::default(); - let mut vkey_to_key = HashMap::default(); - let mut vkey_to_shifted = HashMap::default(); - for vkey in CANDIDATE_VKEYS { - if let Some(key) = get_key_from_vkey(*vkey) { - key_to_vkey.insert(key.clone(), (vkey.0, false)); - vkey_to_key.insert(vkey.0, key); - } - let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; - if scan_code == 0 { - continue; - } - if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { - key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); - vkey_to_shifted.insert(vkey.0, shifted_key); - } - } - Self { - key_to_vkey, - vkey_to_key, - vkey_to_shifted, - } - } - - fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { - if use_key_equivalents { - get_vkey_from_key_with_us_layout(key) - } else { - self.key_to_vkey.get(key).cloned() - } - } -} - pub(crate) fn get_keystroke_key( vkey: VIRTUAL_KEY, scan_code: u32, @@ -243,134 +140,3 @@ pub(crate) fn generate_key_char( _ => None, } } - -fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { - match key { - // ` => VK_OEM_3 - "`" => Some((VK_OEM_3.0, false)), - "~" => Some((VK_OEM_3.0, true)), - "1" => Some((VK_1.0, false)), - "!" => Some((VK_1.0, true)), - "2" => Some((VK_2.0, false)), - "@" => Some((VK_2.0, true)), - "3" => Some((VK_3.0, false)), - "#" => Some((VK_3.0, true)), - "4" => Some((VK_4.0, false)), - "$" => Some((VK_4.0, true)), - "5" => Some((VK_5.0, false)), - "%" => Some((VK_5.0, true)), - "6" => Some((VK_6.0, false)), - "^" => Some((VK_6.0, true)), - "7" => Some((VK_7.0, false)), - "&" => Some((VK_7.0, true)), - "8" => Some((VK_8.0, false)), - "*" => Some((VK_8.0, true)), - "9" => Some((VK_9.0, false)), - "(" => Some((VK_9.0, true)), - "0" => Some((VK_0.0, false)), - ")" => Some((VK_0.0, true)), - "-" => Some((VK_OEM_MINUS.0, false)), - "_" => Some((VK_OEM_MINUS.0, true)), - "=" => Some((VK_OEM_PLUS.0, false)), - "+" => Some((VK_OEM_PLUS.0, true)), - "[" => Some((VK_OEM_4.0, false)), - "{" => Some((VK_OEM_4.0, true)), - "]" => Some((VK_OEM_6.0, false)), - "}" => Some((VK_OEM_6.0, true)), - "\\" => Some((VK_OEM_5.0, false)), - "|" => Some((VK_OEM_5.0, true)), - ";" => Some((VK_OEM_1.0, false)), - ":" => Some((VK_OEM_1.0, true)), - "'" => Some((VK_OEM_7.0, false)), - "\"" => Some((VK_OEM_7.0, true)), - "," => Some((VK_OEM_COMMA.0, false)), - "<" => Some((VK_OEM_COMMA.0, true)), - "." => Some((VK_OEM_PERIOD.0, false)), - ">" => Some((VK_OEM_PERIOD.0, true)), - "/" => Some((VK_OEM_2.0, false)), - "?" => Some((VK_OEM_2.0, true)), - _ => None, - } -} - -const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ - VK_OEM_3, - VK_OEM_MINUS, - VK_OEM_PLUS, - VK_OEM_4, - VK_OEM_5, - VK_OEM_6, - VK_OEM_1, - VK_OEM_7, - VK_OEM_COMMA, - VK_OEM_PERIOD, - VK_OEM_2, - VK_OEM_102, - VK_OEM_8, - VK_ABNT_C1, - VK_0, - VK_1, - VK_2, - VK_3, - VK_4, - VK_5, - VK_6, - VK_7, - VK_8, - VK_9, -]; - -#[cfg(test)] -mod tests { - use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; - - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); - - // Normal case - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "a".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "a"); - assert_eq!(mapped.display_modifiers, Modifiers::control()); - - // Shifted case, ctrl-$ - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Shifted case, but shift is true - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Windows style - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "4".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.inner.key, "$"); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - } -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f23..6202e05fb3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -351,10 +351,6 @@ impl Platform for WindowsPlatform { ) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 2d4abf4063..46d5eafd5a 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,7 +13,6 @@ 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 8384f2a88e..fffe18a616 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,28 +52,6 @@ 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/icons/src/icons.rs b/crates/icons/src/icons.rs index f7363395ae..38f02c2206 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -34,7 +34,6 @@ pub enum IconName { ArrowRightLeft, ArrowUp, ArrowUpRight, - Attach, AudioOff, AudioOn, Backspace, @@ -165,7 +164,6 @@ pub enum IconName { PageDown, PageUp, Pencil, - PencilUnavailable, Person, Pin, PlayOutlined, @@ -215,7 +213,6 @@ pub enum IconName { Tab, Terminal, TerminalAlt, - TerminalGhost, TextSnippet, TextThread, Thread, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 2dca57424b..b96557b391 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,19 +401,12 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - pub struct ImageViewerDb(ThreadSafeConnection); - - impl Domain for ImageViewerDb { - const NAME: &str = stringify!(ImageViewerDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref IMAGE_VIEWER: ImageViewerDb = + &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -424,11 +417,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); - impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index cefe888974..8e55a8a477 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,7 +24,6 @@ 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 c3d687e57a..0c2b16b9f4 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 = util_macros::path!("/zed-inspector-style.json"); +const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; pub(crate) struct DivInspector { state: State, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..b106110c33 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1569,21 +1569,11 @@ impl Buffer { self.send_operation(op, true, cx); } - 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(), - } + 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) } fn request_autoindent(&mut self, cx: &mut Context) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0f82d3997f..386ad19747 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -350,12 +350,6 @@ 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 @@ -365,7 +359,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default)] + #[serde(default = "default_lsp_fetch_timeout_ms")] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -411,8 +405,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_3() -> usize { - 3 +fn default_lsp_fetch_timeout_ms() -> u64 { + 0 } /// The settings for a particular language. @@ -1474,7 +1468,6 @@ 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, diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index b06a475f93..ebfd37d16c 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,16 +4,12 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; -use anyhow::anyhow; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; use smol::stream::StreamExt; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, -}; +use std::sync::Arc; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -110,7 +106,6 @@ pub struct FakeLanguageModel { >, )>, >, - forbid_requests: AtomicBool, } impl Default for FakeLanguageModel { @@ -119,20 +114,11 @@ impl Default for FakeLanguageModel { provider_id: LanguageModelProviderId::from("fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()), current_completion_txs: Mutex::new(Vec::new()), - forbid_requests: AtomicBool::new(false), } } } impl FakeLanguageModel { - pub fn allow_requests(&self) { - self.forbid_requests.store(false, SeqCst); - } - - pub fn forbid_requests(&self) { - self.forbid_requests.store(true, SeqCst); - } - pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() @@ -265,18 +251,9 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - if self.forbid_requests.load(SeqCst) { - async move { - Err(LanguageModelCompletionError::Other(anyhow!( - "requests are forbidden" - ))) - } - .boxed() - } else { - let (tx, rx) = mpsc::unbounded(); - self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.boxed()) }.boxed() - } + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { Ok(rx.boxed()) }.boxed() } fn as_fake(&self) -> &Self { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index d5313b6a3a..158bebcbbf 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::{AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -640,14 +640,24 @@ 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)] +#[derive(Default, Clone, Copy)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, - Other(SharedString), + Other(&'static str), } #[derive(PartialEq, Eq)] diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 531c3615dc..8f52f8c1c3 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -25,6 +25,9 @@ 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 { @@ -35,6 +38,9 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } + Self::ProviderPendingTermsAcceptance(provider) => { + write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) + } } } } @@ -194,6 +200,12 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } + if model.provider.must_accept_terms(cx) { + return Some(ConfigurationError::ProviderPendingTermsAcceptance( + model.provider, + )); + } + None } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index c492edeaf5..0d061c0587 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".into(), - ConfigurationViewTargetAgent::Other(agent) => agent.clone(), + .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( List::new() diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b473d06357..b1b5ff3eb3 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, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, - PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, + ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -118,6 +118,7 @@ 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>, @@ -141,6 +142,7 @@ impl State { llm_api_token: LlmApiToken::default(), user_store, status, + accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, @@ -195,6 +197,24 @@ 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(); @@ -364,7 +384,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) + !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -381,11 +401,112 @@ 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, @@ -986,7 +1107,10 @@ 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, } @@ -1052,30 +1176,58 @@ impl RenderOnce for ZedAiConfiguration { ); } - 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) - } - }) + 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) } } 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| { @@ -1087,6 +1239,7 @@ impl ConfigurationView { Self { state, + accept_terms_of_service_callback, sign_in_callback, } } @@ -1102,7 +1255,10 @@ 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(), } } @@ -1127,6 +1283,7 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, + has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1135,7 +1292,10 @@ 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() @@ -1146,30 +1306,33 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example("Not connected", configuration(false, None, false, false)), + single_example( + "Not connected", + configuration(false, None, false, false, true), + ), single_example( "Accept Terms of Service", - configuration(true, None, true, false), + configuration(true, None, true, false, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false), + configuration(true, None, false, false, true), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false), + configuration(true, None, true, false, true), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false), + configuration(true, Some(Plan::ZedFree), true, false, true), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false), + configuration(true, Some(Plan::ZedProTrial), true, false, true), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false), + configuration(true, Some(Plan::ZedPro), true, false, true), ), ]) .into_any_element(), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index f252ab7aa3..566620675e 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -921,9 +921,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 Google AI".into(), - ConfigurationViewTargetAgent::Other(agent) => agent.clone(), + .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", + ConfigurationViewTargetAgent::Other(agent) => agent, }))) .child( List::new() diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4140713544..057259d114 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,6 +4,7 @@ use gpui::{ }; use itertools::Itertools; use serde_json::json; +use settings::get_key_equivalents; use ui::{Button, ButtonStyle}; use ui::{ ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, @@ -168,8 +169,7 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - - let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); + let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,5 +1743,6 @@ pub enum Event { } impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index ebeac7efff..9d5ebbaf71 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,7 +231,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -251,4 +250,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index dbec1937b1..7baba5f227 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,21 +11,6 @@ (#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/rust.rs b/crates/languages/src/rust.rs index 3e8dce756b..c6c7357148 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -510,6 +510,20 @@ 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) } } diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index f7cb987831..5e2fbbf63a 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,7 +237,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -257,4 +256,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 9eec01cc89..48da80995b 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,21 +11,6 @@ (#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/highlights.scm b/crates/languages/src/typescript/highlights.scm index 84cbbae77d..af37ef6415 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,7 +248,6 @@ "is" "keyof" "let" - "module" "namespace" "new" "of" @@ -273,4 +272,4 @@ "while" "with" "yield" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 1ca1e9ad59..7affdc5b75 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,21 +15,6 @@ (#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/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f607a033a..755506bd12 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_4() + .w_5() .absolute() - .top_1p5() - .right_1p5() - .justify_end() + .top_1() + .right_1() + .justify_center() .child(codeblock), ) }); @@ -1115,12 +1115,11 @@ impl Element for MarkdownElement { cx, ); el.child( - h_flex() - .w_4() + div() .absolute() .top_0() .right_0() - .justify_end() + .w_5() .visible_on_hover("code_block") .child(codeblock), ) @@ -1321,9 +1320,8 @@ 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")) + .tooltip(Tooltip::text("Copy Code")) .on_click({ let markdown = markdown; move |_event, _window, cx| { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a..a54d38163d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); + let mut buffer_ids = Vec::new(); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 54c49bc72a..672bcf1cd9 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,13 +283,17 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child( - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) - .tab_index(Some({ - tab_index += 1; - tab_index - 1 - })), - ) + .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(render_llm_provider_section( &mut tab_index, workspace, diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 47dfd84894..8fae695854 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -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/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201..884374a72f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref ONBOARDING_PAGES: OnboardingPagesDb = + &[ + sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -872,11 +866,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + ), + ]; } - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 8ff55d812b..3fe9c32a48 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -436,11 +430,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]); + ), + ]; } - db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); - impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 08be82b830..acf6ec434a 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,6 +446,7 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { + pub model: String, pub choices: Vec, pub usage: Option, } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4..834bf2c2d2 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::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -254,18 +254,14 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style, ssh_shell) = + let (mut ssh_command, envs, path_style) = ssh_client.read_with(cx, |ssh, _| { - let SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - } = ssh.ssh_info().context("SSH arguments not found")?; + let (SshArgs { arguments, envs }, path_style) = + ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, - shell, )) })??; @@ -284,7 +280,6 @@ impl DapStore { } let (program, args) = wrap_for_ssh( - &ssh_shell, &ssh_command, binary .command diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index b2f9580f9c..3e28fac8af 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(None, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(true, &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/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..cc3a0a05bb 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7588,16 +7588,19 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .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(), - } + .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(), + } + }) }) .collect::>(); @@ -9026,22 +9029,13 @@ 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 = 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)) - }) + 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)) .transpose()? } else { None @@ -11703,11 +11697,12 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(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 { @@ -11731,11 +11726,12 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + 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); + } } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11750,32 +11746,36 @@ impl LspStore { } } "textDocument/formatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/rename" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/inlayHint" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/documentSymbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(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 @@ -11791,11 +11791,12 @@ impl LspStore { } } "textDocument/definition" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(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 @@ -11903,7 +11904,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12054,7 +12055,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); @@ -12174,10 +12175,10 @@ impl LspStore { // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> Result> { +) -> anyhow::Result>> { Ok(match reg.register_options { - Some(options) => OneOf::Right(serde_json::from_value::(options)?), - None => OneOf::Left(true), + Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), + None => Some(OneOf::Left(true)), }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 54f63220b1..e5e6338d3c 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, AsyncApp, Entity, Task, WeakEntity}; -use language::{Buffer, ServerHealth}; -use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; +use gpui::{App, Entity, Task, WeakEntity}; +use language::ServerHealth; +use lsp::{LanguageServer, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -83,32 +83,31 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, 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 = 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) - }) + let buffer = 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 = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), 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) + })? 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,33 +130,28 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, 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 = 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) - }) + let buffer = 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 = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), 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) + })? 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, @@ -188,32 +182,31 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, 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 = 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) - }) + let buffer = 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 = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), 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) + })? 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 @@ -233,40 +226,3 @@ 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/project_settings.rs b/crates/project/src/project_settings.rs index 4447c25129..a6fea4059c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -181,6 +181,17 @@ pub struct DiagnosticsSettings { /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, + + /// Configuration, related to Rust language diagnostics. + pub cargo: Option, +} + +impl DiagnosticsSettings { + pub fn fetch_cargo_diagnostics(&self) -> bool { + self.cargo + .as_ref() + .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] @@ -247,6 +258,7 @@ impl Default for DiagnosticsSettings { include_warnings: true, lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), inline: InlineDiagnosticsSettings::default(), + cargo: None, } } } @@ -280,6 +292,16 @@ impl Default for GlobalLspSettings { } } +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CargoDiagnosticsSettings { + /// When enabled, Zed disables rust-analyzer's check on save and starts to query + /// Cargo diagnostics separately. + /// + /// Default: false + #[serde(default)] + pub fetch_cargo_diagnostics: bool, +} + #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, )] diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b009b357fe..e9582e73fd 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; -use remote::{SshInfo, ssh_session::SshArgs}; +use remote::ssh_session::SshArgs; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -13,7 +13,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, SpawnInTerminal}; +use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, @@ -58,13 +58,11 @@ impl SshCommand { } } -#[derive(Debug)] pub struct SshDetails { pub host: String, pub ssh_command: SshCommand, pub envs: Option>, pub path_style: PathStyle, - pub shell: String, } impl Project { @@ -89,18 +87,12 @@ impl Project { pub fn ssh_details(&self, cx: &App) -> Option { if let Some(ssh_client) = &self.ssh_client { let ssh_client = ssh_client.read(cx); - if let Some(SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - }) = ssh_client.ssh_info() - { + if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { return Some(SshDetails { host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, - shell, }); } } @@ -173,9 +165,7 @@ impl Project { let ssh_details = self.ssh_details(cx); let settings = self.terminal_settings(&path, cx).clone(); - let builder = - ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell) - .non_interactive(); + let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self @@ -190,11 +180,9 @@ impl Project { ssh_command, envs, path_style, - shell, .. }) => { let (command, args) = wrap_for_ssh( - &shell, &ssh_command, Some((&command, &args)), path.as_deref(), @@ -292,7 +280,6 @@ impl Project { ssh_command, envs, path_style, - shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); @@ -304,7 +291,6 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( - &shell, &ssh_command, None, path.as_deref(), @@ -357,13 +343,11 @@ impl Project { ssh_command, envs, path_style, - shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( - &shell, &ssh_command, spawn_task .command @@ -653,7 +637,6 @@ impl Project { } pub fn wrap_for_ssh( - shell: &str, ssh_command: &SshCommand, command: Option<(&String, &Vec)>, path: Option<&Path>, @@ -662,11 +645,16 @@ pub fn wrap_for_ssh( path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { - let command: Option> = shlex::try_quote(command).ok(); + // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped + let command: Option> = if command == DEFAULT_REMOTE_SHELL { + Some(command.into()) + } else { + shlex::try_quote(command).ok() + }; let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); command.into_iter().chain(args).join(" ") } else { - format!("exec {shell} -l") + "exec ${SHELL:-sh} -l".to_string() }; let mut env_changes = String::new(); @@ -700,7 +688,7 @@ pub fn wrap_for_ssh( } else { format!("cd; {env_changes} {to_run}") }; - let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); + let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap()); let program = "ssh".to_string(); let mut args = ssh_command.arguments.clone(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5a30a3e9bc..52ec7a9880 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,7 +69,6 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; -use zed_actions::workspace::OpenWithSystem; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -256,6 +255,8 @@ actions!( RevealInFileManager, /// Removes the selected folder from the project. RemoveFromProject, + /// Opens the selected file with the system's default application. + OpenWithSystem, /// Cuts the selected file or directory. Cut, /// Pastes the previously cut or copied item. @@ -4089,7 +4090,6 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4223,7 +4223,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - )) + ) }) .on_mouse_down( MouseButton::Left, @@ -4434,7 +4434,6 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div - .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4466,7 +4465,7 @@ impl ProjectPanel { } }, - ))) + )) }) .child( Label::new(DELIMITER.clone()) @@ -4486,7 +4485,6 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div - .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4524,7 +4522,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - })) + }) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -5032,8 +5030,7 @@ impl ProjectPanel { sticky_parents.reverse(); - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; + let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5117,11 +5114,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let panel_settings = ProjectPanelSettings::get_global(cx); - let indent_size = panel_settings.indent_size; - let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; + let indent_size = ProjectPanelSettings::get_global(cx).indent_size; + let show_indent_guides = + ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if panel_settings.sticky_scroll { + if ProjectPanelSettings::get_global(cx).sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5209,10 +5206,8 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .when(panel_settings.drag_and_drop, |this| { - this.on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) - }) + .on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5550,32 +5545,30 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.when(panel_settings.drag_and_drop, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7..8a243589ed 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -47,7 +47,6 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, - pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -161,10 +160,6 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, } impl Settings for ProjectPanelSettings { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38c..ac9c275aa2 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -834,19 +834,21 @@ message LspRunnable { message LspExtCancelFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspExtRunFlycheck { uint64 project_id = 1; - optional uint64 buffer_id = 2; + uint64 buffer_id = 2; uint64 language_server_id = 3; bool current_file_only = 4; } message LspExtClearFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspDiagnosticRelatedInformation { diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 8ffe0ef07c..dd4d788cfd 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; use remote::SshConnectionOptions; @@ -101,17 +103,17 @@ impl DisconnectedOverlay { return; }; + let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else { + return; + }; + let Some(window_handle) = window.window_handle().downcast::() else { return; }; let app_state = workspace.read(cx).app_state().clone(); - let paths = workspace - .read(cx) - .root_paths(cx) - .iter() - .map(|path| path.to_path_buf()) - .collect(); + + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fa57b588cd..2093e96cae 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -19,12 +19,15 @@ use picker::{ pub use remote_servers::RemoteServerProjects; use settings::Settings; pub use ssh_connections::SshSettings; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, - WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace, + CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, + Workspace, WorkspaceId, with_active_or_new_workspace, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -151,7 +154,7 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, @@ -175,15 +178,12 @@ impl RecentProjectsDelegate { } } - pub fn set_workspaces( - &mut self, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - ) { + pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { self.workspaces = workspaces; self.has_any_non_local_projects = !self .workspaces .iter() - .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); + .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _))); } } impl EventEmitter for RecentProjectsDelegate {} @@ -236,14 +236,15 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, _, paths))| { - let combined_string = paths - .paths() + .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx)) + .map(|(id, (_, location))| { + let combined_string = location + .sorted_paths() .iter() .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); + StringMatchCandidate::new(id, &combined_string) }) .collect::>(); @@ -278,7 +279,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = + let (candidate_workspace_id, candidate_workspace_location) = &self.workspaces[selected_match.candidate_id]; let replace_current_window = if self.create_new_window { secondary @@ -291,8 +292,8 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(Ok(())) } else { match candidate_workspace_location { - SerializedWorkspaceLocation::Local => { - let paths = candidate_workspace_paths.paths().to_vec(); + SerializedWorkspaceLocation::Local(paths, _) => { + let paths = paths.paths().to_vec(); if replace_current_window { cx.spawn_in(window, async move |workspace, cx| { let continue_replacing = workspace @@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(connection) => { + SerializedWorkspaceLocation::Ssh(ssh_project) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -336,12 +337,12 @@ impl PickerDelegate for RecentProjectsDelegate { let connection_options = SshSettings::get_global(cx) .connection_options_for( - connection.host.clone(), - connection.port, - connection.user.clone(), + ssh_project.host.clone(), + ssh_project.port, + ssh_project.user.clone(), ); - let paths = candidate_workspace_paths.paths().to_vec(); + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( @@ -382,12 +383,12 @@ impl PickerDelegate for RecentProjectsDelegate { ) -> Option { let hit = self.matches.get(ix)?; - let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; + let (_, location) = self.workspaces.get(hit.candidate_id)?; let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = paths - .paths() + let (match_labels, paths): (Vec<_>, Vec<_>) = location + .sorted_paths() .iter() .map(|p| p.compact()) .map(|path| { @@ -415,9 +416,11 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_3() .when(self.has_any_non_local_projects, |this| { this.child(match location { - SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element(), + SerializedWorkspaceLocation::Local(_, _) => { + Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element() + } SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) .color(Color::Muted) .into_any_element(), @@ -565,7 +568,7 @@ impl RecentProjectsDelegate { cx: &mut Context>, ) { if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; + let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; cx.spawn_in(window, async move |this, cx| { let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB @@ -704,8 +707,7 @@ mod tests { }]; delegate.set_workspaces(vec![( WorkspaceId::default(), - SerializedWorkspaceLocation::Local, - PathList::new(&[path!("/test/path")]), + SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]), )]); }); }) diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 71895f1678..43eb59c0ae 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -4,6 +4,6 @@ pub mod proxy; pub mod ssh_session; pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, - SshRemoteClient, SshRemoteEvent, + ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, + SshRemoteEvent, }; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 6794018470..a26f4be661 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -52,6 +52,11 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub struct SshProjectId(pub u64); + #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, @@ -84,19 +89,11 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] pub struct SshArgs { pub arguments: Vec, pub envs: Option>, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshInfo { - pub args: SshArgs, - pub path_style: PathStyle, - pub shell: String, -} - #[macro_export] macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ @@ -445,7 +442,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -474,16 +471,6 @@ impl SshSocket { Ok(SshPlatform { os, arch }) } - - async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { - Ok(shell) => shell.trim().to_owned(), - Err(e) => { - log::error!("Failed to get shell: {e}"); - "sh".to_owned() - } - } - } } const MAX_MISSED_HEARTBEATS: usize = 5; @@ -1165,16 +1152,12 @@ impl SshRemoteClient { cx.notify(); } - pub fn ssh_info(&self) -> Option { + pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { self.state .lock() .as_ref() .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| SshInfo { - args: ssh_connection.ssh_args(), - path_style: ssh_connection.path_style(), - shell: ssh_connection.shell(), - }) + .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) } pub fn upload_directory( @@ -1409,7 +1392,6 @@ trait RemoteConnection: Send + Sync { fn ssh_args(&self) -> SshArgs; fn connection_options(&self) -> SshConnectionOptions; fn path_style(&self) -> PathStyle; - fn shell(&self) -> String; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1421,7 +1403,6 @@ struct SshRemoteConnection { remote_binary_path: Option, ssh_platform: SshPlatform, ssh_path_style: PathStyle, - ssh_shell: String, _temp_dir: TempDir, } @@ -1448,10 +1429,6 @@ impl RemoteConnection for SshRemoteConnection { self.socket.connection_options.clone() } - fn shell(&self) -> String { - self.ssh_shell.clone() - } - fn upload_directory( &self, src_path: PathBuf, @@ -1533,7 +1510,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) + .ssh_command("sh", &["-c", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1665,7 +1642,6 @@ impl SshRemoteConnection { "windows" => PathStyle::Windows, _ => PathStyle::Posix, }; - let ssh_shell = socket.shell().await; let mut this = Self { socket, @@ -1674,7 +1650,6 @@ impl SshRemoteConnection { remote_binary_path: None, ssh_path_style, ssh_platform, - ssh_shell, }; let (release_channel, version, commit) = cx.update(|cx| { @@ -1910,7 +1885,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1963,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2011,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-lc", &script]).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } @@ -2711,10 +2686,6 @@ mod fake { fn path_style(&self) -> PathStyle { PathStyle::current() } - - fn shell(&self) -> String { - "sh".to_owned() - } } pub(super) struct Delegate; diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..dcec9f6fe0 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -65,7 +65,6 @@ telemetry_events.workspace = true util.workspace = true watch.workspace = true worktree.workspace = true -thiserror.workspace = true [target.'cfg(not(windows))'.dependencies] crashes.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 368c7cb639..03b0c3eda3 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,7 +1,6 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use clap::Parser; -use remote_server::Commands; +use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] @@ -22,34 +21,105 @@ struct Cli { printenv: bool, } +#[derive(Subcommand)] +enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + #[arg(long)] + stderr_socket: PathBuf, + }, + Proxy { + #[arg(long)] + reconnect: bool, + #[arg(long)] + identifier: String, + }, + Version, +} + #[cfg(windows)] fn main() { unimplemented!() } #[cfg(not(windows))] -fn main() -> anyhow::Result<()> { +fn main() { + use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; + use remote::proxy::ProxyLaunchError; + use remote_server::unix::{execute_proxy, execute_run}; + let cli = Cli::parse(); if let Some(socket_path) = &cli.askpass { askpass::main(socket_path); - return Ok(()); + return; } if let Some(socket) = &cli.crash_handler { crashes::crash_server(socket.as_path()); - return Ok(()); + return; } if cli.printenv { util::shell_env::print_env(); - return Ok(()); + return; } - if let Some(command) = cli.command { - remote_server::run(command) - } else { - eprintln!("usage: remote "); + let result = match cli.command { + Some(Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + }) => execute_run( + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + ), + Some(Commands::Proxy { + identifier, + reconnect, + }) => match execute_proxy(identifier, reconnect) { + Ok(_) => Ok(()), + Err(err) => { + if let Some(err) = err.downcast_ref::() { + std::process::exit(err.to_exit_code()); + } + Err(err) + } + }, + Some(Commands::Version) => { + let release_channel = *RELEASE_CHANNEL; + match release_channel { + ReleaseChannel::Stable | ReleaseChannel::Preview => { + println!("{}", env!("ZED_PKG_VERSION")) + } + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + println!( + "{}", + option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) + ) + } + }; + std::process::exit(0); + } + None => { + eprintln!("usage: remote "); + std::process::exit(1); + } + }; + if let Err(error) = result { + log::error!("exiting due to error: {}", error); std::process::exit(1); } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index c14a4828ac..52003969af 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,78 +6,4 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; -use clap::Subcommand; -use std::path::PathBuf; - pub use headless_project::{HeadlessAppState, HeadlessProject}; - -#[derive(Subcommand)] -pub enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - -#[cfg(not(windows))] -pub fn run(command: Commands) -> anyhow::Result<()> { - use anyhow::Context; - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use unix::{ExecuteProxyError, execute_proxy, execute_run}; - - match command { - Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - } => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Commands::Proxy { - identifier, - reconnect, - } => execute_proxy(identifier, reconnect) - .inspect_err(|err| { - if let ExecuteProxyError::ServerNotRunning(err) = err { - std::process::exit(err.to_exit_code()); - } - }) - .context("running proxy on the remote server"), - Commands::Version => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - println!( - "{}", - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) - ) - } - }; - Ok(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index c6d1566d60..b8a7351552 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -36,7 +36,6 @@ use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::ffi::OsStr; use std::ops::ControlFlow; -use std::process::ExitStatus; use std::str::FromStr; use std::sync::LazyLock; use std::{env, thread}; @@ -47,7 +46,6 @@ use std::{ sync::Arc, }; use telemetry_events::LocationData; -use thiserror::Error; use util::ResultExt; pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { @@ -528,23 +526,7 @@ pub fn execute_run( Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum ServerPathError { - #[error("Failed to create server_dir `{path}`")] - CreateServerDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, - #[error("Failed to create logs_dir `{path}`")] - CreateLogsDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, -} - -#[derive(Clone, Debug)] +#[derive(Clone)] struct ServerPaths { log_file: PathBuf, pid_file: PathBuf, @@ -554,19 +536,10 @@ struct ServerPaths { } impl ServerPaths { - fn new(identifier: &str) -> Result { + fn new(identifier: &str) -> Result { let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir).map_err(|source| { - ServerPathError::CreateServerDir { - source, - path: server_dir.clone(), - } - })?; - let log_dir = logs_dir(); - std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { - source: source, - path: log_dir.clone(), - })?; + std::fs::create_dir_all(&server_dir)?; + std::fs::create_dir_all(&logs_dir())?; let pid_file = server_dir.join("server.pid"); let stdin_socket = server_dir.join("stdin.sock"); @@ -584,43 +557,7 @@ impl ServerPaths { } } -#[derive(Debug, Error)] -pub(crate) enum ExecuteProxyError { - #[error("Failed to init server paths")] - ServerPath(#[from] ServerPathError), - - #[error(transparent)] - ServerNotRunning(#[from] ProxyLaunchError), - - #[error("Failed to check PidFile '{path}'")] - CheckPidFile { - #[source] - source: CheckPidError, - path: PathBuf, - }, - - #[error("Failed to kill existing server with pid '{pid}'")] - KillRunningServer { - #[source] - source: std::io::Error, - pid: u32, - }, - - #[error("failed to spawn server")] - SpawnServer(#[source] SpawnServerError), - - #[error("stdin_task failed")] - StdinTask(#[source] anyhow::Error), - #[error("stdout_task failed")] - StdoutTask(#[source] anyhow::Error), - #[error("stderr_task failed")] - StderrTask(#[source] anyhow::Error), -} - -pub(crate) fn execute_proxy( - identifier: String, - is_reconnecting: bool, -) -> Result<(), ExecuteProxyError> { +pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { init_logging_proxy(); let server_paths = ServerPaths::new(&identifier)?; @@ -637,19 +574,12 @@ pub(crate) fn execute_proxy( log::info!("starting proxy process. PID: {}", std::process::id()); - let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| { - ExecuteProxyError::CheckPidFile { - source, - path: server_paths.pid_file.clone(), - } - })?; + let server_pid = check_pid_file(&server_paths.pid_file)?; let server_running = server_pid.is_some(); if is_reconnecting { if !server_running { log::error!("attempted to reconnect, but no server running"); - return Err(ExecuteProxyError::ServerNotRunning( - ProxyLaunchError::ServerNotRunning, - )); + anyhow::bail!(ProxyLaunchError::ServerNotRunning); } } else { if let Some(pid) = server_pid { @@ -660,7 +590,7 @@ pub(crate) fn execute_proxy( kill_running_server(pid, &server_paths)?; } - spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?; + spawn_server(&server_paths)?; }; let stdin_task = smol::spawn(async move { @@ -700,9 +630,9 @@ pub(crate) fn execute_proxy( if let Err(forwarding_result) = smol::block_on(async move { futures::select! { - result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), - result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), - result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), + result = stdin_task.fuse() => result.context("stdin_task failed"), + result = stdout_task.fuse() => result.context("stdout_task failed"), + result = stderr_task.fuse() => result.context("stderr_task failed"), } }) { log::error!( @@ -715,12 +645,12 @@ pub(crate) fn execute_proxy( Ok(()) } -fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { +fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") .arg(pid.to_string()) .output() - .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; + .context("failed to kill existing server")?; for file in [ &paths.pid_file, @@ -734,39 +664,18 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxy Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum SpawnServerError { - #[error("failed to remove stdin socket")] - RemoveStdinSocket(#[source] std::io::Error), - - #[error("failed to remove stdout socket")] - RemoveStdoutSocket(#[source] std::io::Error), - - #[error("failed to remove stderr socket")] - RemoveStderrSocket(#[source] std::io::Error), - - #[error("failed to get current_exe")] - CurrentExe(#[source] std::io::Error), - - #[error("failed to launch server process")] - ProcessStatus(#[source] std::io::Error), - - #[error("failed to launch and detach server process: {status}\n{paths}")] - LaunchStatus { status: ExitStatus, paths: String }, -} - -fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { +fn spawn_server(paths: &ServerPaths) -> Result<()> { if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; + std::fs::remove_file(&paths.stdin_socket)?; } if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; + std::fs::remove_file(&paths.stdout_socket)?; } if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; + std::fs::remove_file(&paths.stderr_socket)?; } - let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; + let binary_name = std::env::current_exe()?; let mut server_process = std::process::Command::new(binary_name); server_process .arg("run") @@ -783,17 +692,11 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { let status = server_process .status() - .map_err(SpawnServerError::ProcessStatus)?; - - if !status.success() { - return Err(SpawnServerError::LaunchStatus { - status, - paths: format!( - "log file: {:?}, pid file: {:?}", - paths.log_file, paths.pid_file, - ), - }); - } + .context("failed to launch server process")?; + anyhow::ensure!( + status.success(), + "failed to launch and detach server process" + ); let mut total_time_waited = std::time::Duration::from_secs(0); let wait_duration = std::time::Duration::from_millis(20); @@ -814,15 +717,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { Ok(()) } -#[derive(Debug, Error)] -#[error("Failed to remove PID file for missing process (pid `{pid}`")] -pub(crate) struct CheckPidError { - #[source] - source: std::io::Error, - pid: u32, -} - -fn check_pid_file(path: &Path) -> Result, CheckPidError> { +fn check_pid_file(path: &Path) -> Result> { let Some(pid) = std::fs::read_to_string(&path) .ok() .and_then(|contents| contents.parse::().ok()) @@ -847,7 +742,7 @@ fn check_pid_file(path: &Path) -> Result, CheckPidError> { log::debug!( "Found PID file, but process with that PID does not exist. Removing PID file." ); - std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; + std::fs::remove_file(&path).context("Failed to remove PID file")?; Ok(None) } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588af..c4ba9b5154 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3905,7 +3905,7 @@ pub mod tests { assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1); - this.activate_previous_item(&Default::default(), window, cx); + this.activate_prev_item(false, window, cx); assert_eq!(this.active_item_index(), 0); }); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); @@ -3940,9 +3940,7 @@ pub mod tests { // Focus the second pane's non-search item window .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| { - pane.activate_next_item(&Default::default(), window, cx) - }); + second_pane.update(cx, |pane, cx| pane.activate_next_item(true, window, cx)); }) .unwrap(); diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs new file mode 100644 index 0000000000..6580137535 --- /dev/null +++ b/crates/settings/src/key_equivalents.rs @@ -0,0 +1,1424 @@ +use collections::HashMap; + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +#[cfg(target_os = "macos")] +pub fn get_key_equivalents(layout: &str) -> Option> { + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} + +#[cfg(not(target_os = "macos"))] +pub fn get_key_equivalents(_layout: &str) -> Option> { + None +} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0e8303c4c1..ae3f42853a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -3,8 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, + KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -212,6 +211,9 @@ impl KeymapFile { } pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { + let key_equivalents = + crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id()); + if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), @@ -253,6 +255,12 @@ impl KeymapFile { } }; + let key_equivalents = if *use_key_equivalents { + key_equivalents.as_ref() + } else { + None + }; + let mut section_errors = String::new(); if !unrecognized_fields.is_empty() { @@ -270,7 +278,7 @@ impl KeymapFile { keystrokes, action, context_predicate.clone(), - *use_key_equivalents, + key_equivalents, cx, ); match result { @@ -328,7 +336,7 @@ impl KeymapFile { keystrokes: &str, action: &KeymapAction, context: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, cx: &App, ) -> std::result::Result { let (build_result, action_input_string) = match &action.0 { @@ -396,9 +404,8 @@ impl KeymapFile { keystrokes, action, context, - use_key_equivalents, + key_equivalents, action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), ) { Ok(key_binding) => key_binding, Err(InvalidKeystrokeError { keystroke }) => { @@ -600,7 +607,6 @@ impl KeymapFile { mut operation: KeybindUpdateOperation<'a>, mut keymap_contents: String, tab_size: usize, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Result { match operation { // if trying to replace a keybinding that is not user-defined, treat it as an add operation @@ -640,7 +646,7 @@ impl KeymapFile { .action_value() .context("Failed to generate target action JSON value")?; let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) else { anyhow::bail!("Failed to find keybinding to remove"); }; @@ -675,7 +681,7 @@ impl KeymapFile { .context("Failed to generate source action JSON value")?; if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) { if target.context == source.context { // if we are only changing the keybinding (common case) @@ -775,7 +781,7 @@ impl KeymapFile { } let use_key_equivalents = from.and_then(|from| { let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; + let (index, _) = find_binding(&keymap, &from, &action_value)?; Some(keymap.0[index].use_key_equivalents) }).unwrap_or(false); if use_key_equivalents { @@ -802,7 +808,6 @@ impl KeymapFile { keymap: &'b KeymapFile, target: &KeybindUpdateTarget<'a>, target_action_value: &Value, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Option<(usize, &'b str)> { let target_context_parsed = KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); @@ -818,11 +823,8 @@ impl KeymapFile { for (keystrokes_str, action) in bindings { let Ok(keystrokes) = keystrokes_str .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) - }) - .collect::, InvalidKeystrokeError>>() + .map(Keystroke::parse) + .collect::, _>>() else { continue; }; @@ -830,7 +832,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.inner.should_match(b)) + .all(|(a, b)| a.should_match(b)) { continue; } @@ -845,7 +847,7 @@ impl KeymapFile { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -914,7 +916,7 @@ impl<'a> KeybindUpdateOperation<'a> { #[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], + pub keystrokes: &'a [Keystroke], pub action_name: &'a str, pub action_arguments: Option<&'a str>, } @@ -939,9 +941,6 @@ impl<'a> KeybindUpdateTarget<'a> { fn keystrokes_unparsed(&self) -> String { let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); for keystroke in self.keystrokes { - // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` - // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` - // by default on Windows. keystrokes.push_str(&keystroke.unparse()); keystrokes.push(' '); } @@ -960,7 +959,7 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum KeybindSource { User, Vim, @@ -1021,7 +1020,7 @@ impl From for KeyBindingMetaIndex { #[cfg(test)] mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; + use gpui::Keystroke; use unindent::Unindent; use crate::{ @@ -1050,27 +1049,16 @@ mod tests { operation: KeybindUpdateOperation, expected: impl ToString, ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); + let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) + .expect("Update succeeded"); pretty_assertions::assert_eq!(expected.to_string(), result); } #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { + fn parse_keystrokes(keystrokes: &str) -> Vec { keystrokes .split(' ') - .map(|s| { - KeybindingKeystroke::new( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) + .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) .collect() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d62..b73ab9ae95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,6 @@ mod base_keymap_setting; mod editable_setting_control; +mod key_equivalents; mod keymap_file; mod settings_file; mod settings_json; @@ -13,6 +14,7 @@ use util::asset_str; pub use base_keymap_setting::*; pub use editable_setting_control::*; +pub use key_equivalents::*; pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, @@ -87,10 +89,7 @@ pub fn default_settings() -> Cow<'static, str> { #[cfg(target_os = "macos")] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; -#[cfg(target_os = "windows")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] +#[cfg(not(target_os = "macos"))] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub fn default_keymap() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 76c7166007..9a2d33ef7c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -12,10 +12,8 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, - KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, - ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, + EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton, + Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; @@ -174,7 +172,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Vec, context: Option, } @@ -184,6 +182,15 @@ struct KeybindConflict { remaining_conflict_amount: usize, } +impl KeybindConflict { + fn from_iter<'a>(mut indices: impl Iterator) -> Option { + indices.next().map(|origin| Self { + first_conflict_index: origin.index, + remaining_conflict_amount: indices.count(), + }) + } +} + #[derive(Clone, Copy, PartialEq)] struct ConflictOrigin { override_source: KeybindSource, @@ -231,21 +238,13 @@ impl ConflictOrigin { #[derive(Default)] struct ConflictState { conflicts: Vec>, - keybind_mapping: ConflictKeybindMapping, + keybind_mapping: HashMap>, has_user_conflicts: bool, } -type ConflictKeybindMapping = HashMap< - Vec, - Vec<( - Option, - Vec, - )>, ->; - impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { - let mut action_keybind_mapping = ConflictKeybindMapping::default(); + let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); let mut largest_index = 0; for (index, binding) in key_bindings @@ -253,48 +252,29 @@ impl ConflictState { .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { - let mapping = binding.get_action_mapping(); - let predicate = mapping - .context - .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); - let entry = action_keybind_mapping - .entry(mapping.keystrokes) - .or_default(); - let origin = ConflictOrigin::new(binding.source, index); - if let Some((_, origins)) = - entry - .iter_mut() - .find(|(other_predicate, _)| match (&predicate, other_predicate) { - (None, None) => true, - (Some(a), Some(b)) => normalized_ctx_eq(a, b), - _ => false, - }) - { - origins.push(origin); - } else { - entry.push((predicate, vec![origin])); - } + action_keybind_mapping + .entry(binding.get_action_mapping()) + .or_default() + .push(ConflictOrigin::new(binding.source, index)); largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; - for entries in action_keybind_mapping.values_mut() { - for (_, indices) in entries.iter_mut() { - indices.sort_unstable_by_key(|origin| origin.override_source); - let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { - continue; - }; + for indices in action_keybind_mapping.values_mut() { + indices.sort_unstable_by_key(|origin| origin.override_source); + let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { + continue; + }; - for origin in indices.iter() { - conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { snd } else { fst }) - } - - has_user_conflicts |= fst.override_source == KeybindSource::User - && snd.override_source == KeybindSource::User; + for origin in indices.iter() { + conflicts[origin.index] = + origin.get_conflict_with(if origin == fst { snd } else { fst }) } + + has_user_conflicts |= fst.override_source == KeybindSource::User + && snd.override_source == KeybindSource::User; } Self { @@ -309,34 +289,15 @@ impl ConflictState { action_mapping: &ActionMapping, keybind_idx: Option, ) -> Option { - let ActionMapping { - keystrokes, - context, - } = action_mapping; - let predicate = context - .as_deref() - .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); - self.keybind_mapping.get(keystrokes).and_then(|entries| { - entries - .iter() - .find_map(|(other_predicate, indices)| { - match (&predicate, other_predicate) { - (None, None) => true, - (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), - _ => false, - } - .then_some(indices) - }) - .and_then(|indices| { - let mut indices = indices + self.keybind_mapping + .get(action_mapping) + .and_then(|indices| { + KeybindConflict::from_iter( + indices .iter() - .filter(|&conflict| Some(conflict.index) != keybind_idx); - indices.next().map(|origin| KeybindConflict { - first_conflict_index: origin.index, - remaining_conflict_amount: indices.count(), - }) - }) - }) + .filter(|&conflict| Some(conflict.index) != keybind_idx), + ) + }) } fn conflict_for_idx(&self, idx: usize) -> Option { @@ -414,14 +375,12 @@ impl Focusable for KeymapEditor { } } /// Helper function to check if two keystroke sequences match exactly -fn keystrokes_match_exactly( - keystrokes1: &[KeybindingKeystroke], - keystrokes2: &[KeybindingKeystroke], -) -> bool { +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { keystrokes1.len() == keystrokes2.len() - && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { - k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers - }) + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) } impl KeymapEditor { @@ -511,7 +470,7 @@ impl KeymapEditor { self.filter_editor.read(cx).text(cx) } - fn current_keystroke_query(&self, cx: &App) -> Vec { + fn current_keystroke_query(&self, cx: &App) -> Vec { match self.search_mode { SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), @@ -532,7 +491,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.inner.unparse()) + .map(|keystroke| keystroke.unparse()) .collect::>() .join(" "); @@ -556,7 +515,7 @@ impl KeymapEditor { async fn update_matches( this: WeakEntity, action_query: String, - keystroke_query: Vec, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let action_query = command_palette::normalize_action_query(&action_query); @@ -605,15 +564,13 @@ impl KeymapEditor { { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; - let matches = query - .inner - .modifiers - .is_subset_of(&keystroke.inner.modifiers) - && ((query.inner.key.is_empty() - || query.inner.key == keystroke.inner.key) - && query.inner.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.inner.key, - )); + let matches = + query.modifiers.is_subset_of(&keystroke.modifiers) + && ((query.key.is_empty() + || query.key == keystroke.key) + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -682,7 +639,7 @@ impl KeymapEditor { .map(KeybindSource::from_meta) .unwrap_or(KeybindSource::Unknown); - let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); + let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == KeybindSource::Vim); @@ -1206,11 +1163,8 @@ impl KeymapEditor { .read(cx) .get_scrollbar_offset(Axis::Vertical), )); - let keyboard_mapper = cx.keyboard_mapper().clone(); - cx.spawn(async move |_, _| { - remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await - }) - .detach_and_notify_err(window, cx); + cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( @@ -1429,7 +1383,7 @@ impl ProcessedBinding { .map(|keybind| keybind.get_action_mapping()) } - fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { + fn keystrokes(&self) -> Option<&[Keystroke]> { self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } @@ -2227,7 +2181,7 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); @@ -2323,7 +2277,6 @@ impl KeybindingEditorModal { }).unwrap_or(Ok(()))?; let create = self.creating; - let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; @@ -2336,7 +2289,6 @@ impl KeybindingEditorModal { new_action_args.as_deref(), &fs, tab_size, - keyboard_mapper.as_ref(), ) .await { @@ -2454,21 +2406,11 @@ impl KeybindingEditorModal { } } -fn remove_key_char( - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - }: KeybindingKeystroke, -) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers: inner.modifiers, - key: inner.key, - key_char: None, - }, - display_modifiers, - display_key, +fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { + Keystroke { + modifiers, + key, + ..Default::default() } } @@ -3011,7 +2953,6 @@ async fn save_keybinding_update( new_args: Option<&str>, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await @@ -3054,13 +2995,9 @@ async fn save_keybinding_update( let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3081,7 +3018,6 @@ async fn remove_keybinding( existing: ProcessedBinding, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); @@ -3105,13 +3041,9 @@ async fn remove_keybinding( }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .context("Failed to update keybinding")?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .context("Failed to update keybinding")?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3157,29 +3089,29 @@ fn collect_contexts_from_assets() -> Vec { queue.push(root_context); while let Some(context) = queue.pop() { match context { - Identifier(ident) => { + gpui::KeyBindingContextPredicate::Identifier(ident) => { contexts.insert(ident); } - Equal(ident_a, ident_b) => { + gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - NotEqual(ident_a, ident_b) => { + gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - Descendant(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - Not(ctx) => { + gpui::KeyBindingContextPredicate::Not(ctx) => { queue.push(*ctx); } - And(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - Or(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } @@ -3194,127 +3126,6 @@ fn collect_contexts_from_assets() -> Vec { contexts } -fn normalized_ctx_eq( - a: &gpui::KeyBindingContextPredicate, - b: &gpui::KeyBindingContextPredicate, -) -> bool { - use gpui::KeyBindingContextPredicate::*; - return match (a, b) { - (Identifier(_), Identifier(_)) => a == b, - (Equal(a_left, a_right), Equal(b_left, b_right)) => { - (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) - } - (NotEqual(a_left, a_right), NotEqual(b_left, b_right)) => { - (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) - } - (Descendant(a_parent, a_child), Descendant(b_parent, b_child)) => { - normalized_ctx_eq(a_parent, b_parent) && normalized_ctx_eq(a_child, b_child) - } - (Not(a_expr), Not(b_expr)) => normalized_ctx_eq(a_expr, b_expr), - // Handle double negation: !(!a) == a - (Not(a_expr), b) if matches!(a_expr.as_ref(), Not(_)) => { - let Not(a_inner) = a_expr.as_ref() else { - unreachable!(); - }; - normalized_ctx_eq(b, a_inner) - } - (a, Not(b_expr)) if matches!(b_expr.as_ref(), Not(_)) => { - let Not(b_inner) = b_expr.as_ref() else { - unreachable!(); - }; - normalized_ctx_eq(a, b_inner) - } - (And(a_left, a_right), And(b_left, b_right)) - if matches!(a_left.as_ref(), And(_, _)) - || matches!(a_right.as_ref(), And(_, _)) - || matches!(b_left.as_ref(), And(_, _)) - || matches!(b_right.as_ref(), And(_, _)) => - { - let mut a_operands = Vec::new(); - flatten_and(a, &mut a_operands); - let mut b_operands = Vec::new(); - flatten_and(b, &mut b_operands); - compare_operand_sets(&a_operands, &b_operands) - } - (And(a_left, a_right), And(b_left, b_right)) => { - (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) - || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) - } - (Or(a_left, a_right), Or(b_left, b_right)) - if matches!(a_left.as_ref(), Or(_, _)) - || matches!(a_right.as_ref(), Or(_, _)) - || matches!(b_left.as_ref(), Or(_, _)) - || matches!(b_right.as_ref(), Or(_, _)) => - { - let mut a_operands = Vec::new(); - flatten_or(a, &mut a_operands); - let mut b_operands = Vec::new(); - flatten_or(b, &mut b_operands); - compare_operand_sets(&a_operands, &b_operands) - } - (Or(a_left, a_right), Or(b_left, b_right)) => { - (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) - || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) - } - _ => false, - }; - - fn flatten_and<'a>( - pred: &'a gpui::KeyBindingContextPredicate, - operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, - ) { - use gpui::KeyBindingContextPredicate::*; - match pred { - And(left, right) => { - flatten_and(left, operands); - flatten_and(right, operands); - } - _ => operands.push(pred), - } - } - - fn flatten_or<'a>( - pred: &'a gpui::KeyBindingContextPredicate, - operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, - ) { - use gpui::KeyBindingContextPredicate::*; - match pred { - Or(left, right) => { - flatten_or(left, operands); - flatten_or(right, operands); - } - _ => operands.push(pred), - } - } - - fn compare_operand_sets( - a: &[&gpui::KeyBindingContextPredicate], - b: &[&gpui::KeyBindingContextPredicate], - ) -> bool { - if a.len() != b.len() { - return false; - } - - // For each operand in a, find a matching operand in b - let mut b_matched = vec![false; b.len()]; - for a_operand in a { - let mut found = false; - for (b_idx, b_operand) in b.iter().enumerate() { - if !b_matched[b_idx] && normalized_ctx_eq(a_operand, b_operand) { - b_matched[b_idx] = true; - found = true; - break; - } - } - if !found { - return false; - } - } - - true - } -} - impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" @@ -3377,15 +3188,12 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl Domain for KeybindingEditorDb { - const NAME: &str = stringify!(KeybindingEditorDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = + &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3394,11 +3202,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); - impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( @@ -3422,152 +3228,3 @@ mod persistence { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalized_ctx_cmp() { - #[track_caller] - fn cmp(a: &str, b: &str) -> bool { - let a = gpui::KeyBindingContextPredicate::parse(a) - .expect("Failed to parse keybinding context a"); - let b = gpui::KeyBindingContextPredicate::parse(b) - .expect("Failed to parse keybinding context b"); - normalized_ctx_eq(&a, &b) - } - - // Basic equality - identical expressions - assert!(cmp("a && b", "a && b")); - assert!(cmp("a || b", "a || b")); - assert!(cmp("a == b", "a == b")); - assert!(cmp("a != b", "a != b")); - assert!(cmp("a > b", "a > b")); - assert!(cmp("!a", "!a")); - - // AND operator - associative/commutative - assert!(cmp("a && b", "b && a")); - assert!(cmp("a && b && c", "c && b && a")); - assert!(cmp("a && b && c", "b && a && c")); - assert!(cmp("a && b && c && d", "d && c && b && a")); - - // OR operator - associative/commutative - assert!(cmp("a || b", "b || a")); - assert!(cmp("a || b || c", "c || b || a")); - assert!(cmp("a || b || c", "b || a || c")); - assert!(cmp("a || b || c || d", "d || c || b || a")); - - // Equality operator - associative/commutative - assert!(cmp("a == b", "b == a")); - assert!(cmp("x == y", "y == x")); - - // Inequality operator - associative/commutative - assert!(cmp("a != b", "b != a")); - assert!(cmp("x != y", "y != x")); - - // Complex nested expressions with associative operators - assert!(cmp("(a && b) || c", "c || (a && b)")); - assert!(cmp("(a && b) || c", "c || (b && a)")); - assert!(cmp("(a || b) && c", "c && (a || b)")); - assert!(cmp("(a || b) && c", "c && (b || a)")); - assert!(cmp("(a && b) || (c && d)", "(c && d) || (a && b)")); - assert!(cmp("(a && b) || (c && d)", "(d && c) || (b && a)")); - - // Multiple levels of nesting - assert!(cmp("((a && b) || c) && d", "d && ((a && b) || c)")); - assert!(cmp("((a && b) || c) && d", "d && (c || (b && a))")); - assert!(cmp("a && (b || (c && d))", "(b || (c && d)) && a")); - assert!(cmp("a && (b || (c && d))", "(b || (d && c)) && a")); - - // Negation with associative operators - assert!(cmp("!a && b", "b && !a")); - assert!(cmp("!a || b", "b || !a")); - assert!(cmp("!(a && b) || c", "c || !(a && b)")); - assert!(cmp("!(a && b) || c", "c || !(b && a)")); - - // Descendant operator (>) - NOT associative/commutative - assert!(cmp("a > b", "a > b")); - assert!(!cmp("a > b", "b > a")); - assert!(!cmp("a > b > c", "c > b > a")); - assert!(!cmp("a > b > c", "a > c > b")); - - // Mixed operators with descendant - assert!(cmp("(a > b) && c", "c && (a > b)")); - assert!(!cmp("(a > b) && c", "c && (b > a)")); - assert!(cmp("(a > b) || (c > d)", "(c > d) || (a > b)")); - assert!(!cmp("(a > b) || (c > d)", "(b > a) || (d > c)")); - - // Negative cases - different operators - assert!(!cmp("a && b", "a || b")); - assert!(!cmp("a == b", "a != b")); - assert!(!cmp("a && b", "a > b")); - assert!(!cmp("a || b", "a > b")); - assert!(!cmp("a == b", "a && b")); - assert!(!cmp("a != b", "a || b")); - - // Negative cases - different operands - assert!(!cmp("a && b", "a && c")); - assert!(!cmp("a && b", "c && d")); - assert!(!cmp("a || b", "a || c")); - assert!(!cmp("a || b", "c || d")); - assert!(!cmp("a == b", "a == c")); - assert!(!cmp("a != b", "a != c")); - assert!(!cmp("a > b", "a > c")); - assert!(!cmp("a > b", "c > b")); - - // Negative cases - with negation - assert!(!cmp("!a", "a")); - assert!(!cmp("!a && b", "a && b")); - assert!(!cmp("!(a && b)", "a && b")); - assert!(!cmp("!a || b", "a || b")); - assert!(!cmp("!(a || b)", "a || b")); - - // Negative cases - complex expressions - assert!(!cmp("(a && b) || c", "(a || b) && c")); - assert!(!cmp("a && (b || c)", "a || (b && c)")); - assert!(!cmp("(a && b) || (c && d)", "(a || b) && (c || d)")); - assert!(!cmp("a > b && c", "a && b > c")); - - // Edge cases - multiple same operands - assert!(cmp("a && a", "a && a")); - assert!(cmp("a || a", "a || a")); - assert!(cmp("a && a && b", "b && a && a")); - assert!(cmp("a || a || b", "b || a || a")); - - // Edge cases - deeply nested - assert!(cmp( - "((a && b) || (c && d)) && ((e || f) && g)", - "((e || f) && g) && ((c && d) || (a && b))" - )); - assert!(cmp( - "((a && b) || (c && d)) && ((e || f) && g)", - "(g && (f || e)) && ((d && c) || (b && a))" - )); - - // Edge cases - repeated patterns - assert!(cmp("(a && b) || (a && b)", "(b && a) || (b && a)")); - assert!(cmp("(a || b) && (a || b)", "(b || a) && (b || a)")); - - // Negative cases - subtle differences - assert!(!cmp("a && b && c", "a && b")); - assert!(!cmp("a || b || c", "a || b")); - assert!(!cmp("(a && b) || c", "a && (b || c)")); - - // a > b > c is not the same as a > c, should not be equal - assert!(!cmp("a > b > c", "a > c")); - - // Double negation with complex expressions - assert!(cmp("!(!(a && b))", "a && b")); - assert!(cmp("!(!(a || b))", "a || b")); - assert!(cmp("!(!(a > b))", "a > b")); - assert!(cmp("!(!a) && b", "a && b")); - assert!(cmp("!(!a) || b", "a || b")); - assert!(cmp("!(!(a && b)) || c", "(a && b) || c")); - assert!(cmp("!(!(a && b)) || c", "(b && a) || c")); - assert!(cmp("!(!a)", "a")); - assert!(cmp("a", "!(!a)")); - assert!(cmp("!(!(!a))", "!a")); - assert!(cmp("!(!(!(!a)))", "a")); - } -} diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ca50d5c03d..1b8010853e 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult { } pub struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, + keystrokes: Vec, + placeholder_keystrokes: Option>, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option, @@ -70,7 +70,7 @@ impl KeystrokeInput { const KEYSTROKE_COUNT_MAX: usize = 3; pub fn new( - placeholder_keystrokes: Option>, + placeholder_keystrokes: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -97,7 +97,7 @@ impl KeystrokeInput { } } - pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { self.keystrokes = keystrokes; self.keystrokes_changed(cx); } @@ -106,7 +106,7 @@ impl KeystrokeInput { self.search = search; } - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { @@ -116,22 +116,18 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.display_key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } &self.keystrokes } - fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }, - display_modifiers: modifiers, - display_key: "".to_string(), + fn dummy(modifiers: Modifiers) -> Keystroke { + Keystroke { + modifiers, + key: "".to_string(), + key_char: None, } } @@ -258,7 +254,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.display_key.is_empty() + && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -267,15 +263,13 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.display_modifiers |= event.modifiers; - last.inner.modifiers |= event.modifiers; + last.modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.display_modifiers = event.modifiers; - last.inner.modifiers = event.modifiers; + last.modifiers = event.modifiers; return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -303,17 +297,14 @@ impl KeystrokeInput { return; } - let mut keystroke = - KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); + let mut keystroke = keystroke.clone(); if let Some(last) = self.keystrokes.last() - && last.display_key.is_empty() + && last.key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let display_key = keystroke.display_key.clone(); - let inner_key = keystroke.inner.key.clone(); + let key = keystroke.key.clone(); keystroke = last.clone(); - keystroke.display_key = display_key; - keystroke.inner.key = inner_key; + keystroke.key = key; self.keystrokes.pop(); } @@ -333,14 +324,11 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if self.search { - self.previous_modifiers = keystroke.display_modifiers; + self.previous_modifiers = keystroke.modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.display_modifiers.modified() - { - self.keystrokes - .push(Self::dummy(keystroke.display_modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); } } @@ -376,7 +364,7 @@ impl KeystrokeInput { &self.keystrokes }; keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keybinding_keystroke( + h_flex().children(ui::render_keystroke( keystroke, Some(Color::Default), Some(rems(0.875).into()), @@ -821,13 +809,9 @@ mod tests { /// Verifies that the keystrokes match the expected strings #[track_caller] pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { - let actual: Vec = self.input.read_with(&self.cx, |input, _| { - input - .keystrokes - .iter() - .map(|keystroke| keystroke.inner.clone()) - .collect() - }); + let actual = self + .input + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -955,7 +939,7 @@ mod tests { } struct KeystrokeUpdateTracker { - initial_keystrokes: Vec, + initial_keystrokes: Vec, _subscription: Subscription, input: Entity, received_keystrokes_updated: bool, @@ -999,8 +983,8 @@ mod tests { ); } - fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { - ks.iter().map(|ks| ks.inner.unparse()).join(" ") + fn keystrokes_str(ks: &[Keystroke]) -> String { + ks.iter().map(|ks| ks.unparse()).join(" ") } } } diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index 5744a67da2..a83f4e18d6 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,12 +1,8 @@ use crate::connection::Connection; pub trait Domain: 'static { - const NAME: &str; - const MIGRATIONS: &[&str]; - - fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { - false - } + fn name() -> &'static str; + fn migrations() -> &'static [&'static str]; } pub trait Migrator: 'static { @@ -21,11 +17,7 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) + connection.migrate(Self::name(), Self::migrations()) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb41..7c59ffe658 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,12 +34,7 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate( - &self, - domain: &'static str, - migrations: &[&'static str], - mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, - ) -> Result<()> { + pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -70,14 +65,9 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; - } else if should_allow_migration_change(index, &completed_migration, &migration) - { - continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -118,7 +108,6 @@ mod test { a TEXT, b TEXT )"}], - disallow_migration_change, ) .unwrap(); @@ -147,7 +136,6 @@ mod test { d TEXT )"}, ], - disallow_migration_change, ) .unwrap(); @@ -226,11 +214,7 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -248,11 +232,7 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -272,28 +252,27 @@ mod test { .migrate( "test migration", &[ - "CREATE TABLE test (col INTEGER)", - "INSERT INTO test (col) VALUES (1)", + indoc! {" + CREATE TABLE test ( + col INTEGER + )"}, + indoc! {" + INSERT INTO test (col) VALUES (1)"}, ], - disallow_migration_change, ) .unwrap(); - let mut migration_changed = false; - // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - "CREATE TABLE test (color INTEGER )", - "INSERT INTO test (color) VALUES (1)", + indoc! {" + CREATE TABLE test ( + color INTEGER + )"}, + indoc! {" + INSERT INTO test (color) VALUES (1)"}, ], - |_, old, new| { - assert_eq!(old, "CREATE TABLE test (col INTEGER)"); - assert_eq!(new, "CREATE TABLE test (color INTEGER)"); - migration_changed = true; - false - }, ); // Verify new migration returns error when run @@ -305,11 +284,7 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .unwrap(); connection @@ -330,7 +305,6 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], - disallow_migration_change, ) .unwrap(); @@ -338,8 +312,4 @@ mod test { assert_eq!(res, "test text"); } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78f..afdc96586e 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,8 +278,12 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; + fn name() -> &'static str { + "test" + } + fn migrations() -> &'static [&'static str] { + &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] + } } for _ in 0..100 { @@ -308,9 +312,12 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - const NAME: &str = "workspace"; + fn name() -> &'static str { + "workspace" + } - const MIGRATIONS: &[&str] = &[" + fn migrations() -> &'static [&'static str] { + &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -329,7 +336,8 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "]; + "] + } } let builder = diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml deleted file mode 100644 index 8ef1b581ae..0000000000 --- a/crates/system_specs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "system_specs" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/system_specs.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -gpui.workspace = true -human_bytes.workspace = true -release_channel.workspace = true -serde.workspace = true -sysinfo.workspace = true -workspace-hack.workspace = true - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -pciid-parser.workspace = true diff --git a/crates/system_specs/LICENSE-GPL b/crates/system_specs/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/system_specs/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index bf3ce7b568..11e32523b4 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -2,14 +2,12 @@ mod tab_switcher_tests; use collections::HashMap; -use editor::items::{ - entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, -}; +use editor::items::entry_git_aware_label_color; use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, - Render, Styled, Task, WeakEntity, Window, actions, rems, + Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, + Styled, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::Project; @@ -17,14 +15,11 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::{cmp::Reverse, sync::Arc}; -use ui::{ - DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, - prelude::*, -}; +use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, + item::{ItemHandle, ItemSettings, TabContentParams}, pane::{Event as PaneEvent, render_item_indicator, tab_details}, }; @@ -118,13 +113,7 @@ impl TabSwitcher { } let weak_workspace = workspace.weak_handle(); - let project = workspace.project().clone(); - let original_items: Vec<_> = workspace - .panes() - .iter() - .map(|p| (p.clone(), p.read(cx).active_item_index())) - .collect(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = TabSwitcherDelegate::new( project, @@ -135,7 +124,6 @@ impl TabSwitcher { is_global, window, cx, - original_items, ); TabSwitcher::new(delegate, window, is_global, cx) }); @@ -233,80 +221,7 @@ pub struct TabSwitcherDelegate { workspace: WeakEntity, project: Entity, matches: Vec, - original_items: Vec<(Entity, usize)>, is_all_panes: bool, - restored_items: bool, -} - -impl TabMatch { - fn icon( - &self, - project: &Entity, - selected: bool, - window: &Window, - cx: &App, - ) -> Option { - let icon = self.item.tab_icon(window, cx)?; - let item_settings = ItemSettings::get_global(cx); - let show_diagnostics = item_settings.show_diagnostics; - let git_status_color = item_settings - .git_status - .then(|| { - let path = self.item.project_path(cx)?; - let project = project.read(cx); - let entry = project.entry_for_path(&path, cx)?; - let git_status = project - .project_path_git_status(&path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some(entry_git_aware_label_color( - git_status, - entry.is_ignored, - selected, - )) - }) - .flatten(); - let colored_icon = icon.color(git_status_color.unwrap_or_default()); - - let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { - None - } else { - let buffer_store = project.read(cx).buffer_store().read(cx); - let buffer = self - .item - .project_path(cx) - .and_then(|path| buffer_store.get_by_path(&path)) - .map(|buffer| buffer.read(cx)); - buffer.and_then(|buffer| { - buffer - .buffer_diagnostics(None) - .iter() - .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) - .min() - }) - }; - - let decorations = - entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) - .filter(|(d, _)| { - *d != IconDecorationKind::Triangle - || show_diagnostics != ShowDiagnostics::Errors - }) - .map(|(icon, color)| { - let knockout_item_color = if selected { - cx.theme().colors().element_selected - } else { - cx.theme().colors().element_background - }; - IconDecoration::new(icon, knockout_item_color, cx) - .color(color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }) - }); - Some(DecoratedIcon::new(colored_icon, decorations)) - } } impl TabSwitcherDelegate { @@ -320,7 +235,6 @@ impl TabSwitcherDelegate { is_all_panes: bool, window: &mut Window, cx: &mut Context, - original_items: Vec<(Entity, usize)>, ) -> Self { Self::subscribe_to_updates(&pane, window, cx); Self { @@ -332,8 +246,6 @@ impl TabSwitcherDelegate { project, matches: Vec::new(), is_all_panes, - original_items, - restored_items: false, } } @@ -388,6 +300,13 @@ impl TabSwitcherDelegate { let matches = if query.is_empty() { let history = workspace.read(cx).recently_activated_items(cx); + for item in &all_items { + eprintln!( + "{:?} {:?}", + item.item.tab_content_text(0, cx), + (Reverse(history.get(&item.item.item_id())), item.item_index) + ) + } all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items @@ -554,25 +473,8 @@ impl PickerDelegate for TabSwitcherDelegate { self.selected_index } - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { self.selected_index = ix; - - let Some(selected_match) = self.matches.get(self.selected_index()) else { - return; - }; - selected_match - .pane - .update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { - pane.activate_item(index, false, false, window, cx); - } - }) - .ok(); cx.notify(); } @@ -599,13 +501,6 @@ impl PickerDelegate for TabSwitcherDelegate { let Some(selected_match) = self.matches.get(self.selected_index()) else { return; }; - - self.restored_items = true; - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } selected_match .pane .update(cx, |pane, cx| { @@ -616,15 +511,7 @@ impl PickerDelegate for TabSwitcherDelegate { .ok(); } - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - if !self.restored_items { - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } - } - + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.tab_switcher .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); @@ -650,7 +537,31 @@ impl PickerDelegate for TabSwitcherDelegate { }; let label = tab_match.item.tab_content(params, window, cx); - let icon = tab_match.icon(&self.project, selected, window, cx); + let icon = tab_match.item.tab_icon(window, cx).map(|icon| { + let git_status_color = ItemSettings::get_global(cx) + .git_status + .then(|| { + tab_match + .item + .project_path(cx) + .as_ref() + .and_then(|path| { + let project = self.project.read(cx); + let entry = project.entry_for_path(path, cx)?; + let git_status = project + .project_path_git_status(path, cx) + .map(|status| status.summary()) + .unwrap_or_default(); + Some((entry, git_status)) + }) + .map(|(entry, git_status)| { + entry_git_aware_label_color(git_status, entry.is_ignored, selected) + }) + }) + .flatten(); + + icon.color(git_status_color.unwrap_or_default()) + }); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { @@ -692,7 +603,7 @@ impl PickerDelegate for TabSwitcherDelegate { .inset(true) .toggle_state(selected) .child(h_flex().w_full().child(label)) - .start_slot::(icon) + .start_slot::(icon) .map(|el| { if self.selected_index == ix { el.end_slot::(close_button) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index de4ddc00f4..770312bafc 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,40 +1,26 @@ use crate::Shell; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ShellKind { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum ShellKind { #[default] Posix, - Csh, - Fish, Powershell, Nushell, Cmd, } impl ShellKind { - pub fn system() -> Self { - Self::new(&system_shell()) - } - - pub fn new(program: &str) -> Self { - #[cfg(windows)] - let (_, program) = program.rsplit_once('\\').unwrap_or(("", program)); - #[cfg(not(windows))] - let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); + fn new(program: &str) -> Self { if program == "powershell" - || program == "powershell.exe" + || program.ends_with("powershell.exe") || program == "pwsh" - || program == "pwsh.exe" + || program.ends_with("pwsh.exe") { ShellKind::Powershell - } else if program == "cmd" || program == "cmd.exe" { + } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell - } else if program == "fish" { - ShellKind::Fish - } else if program == "csh" { - ShellKind::Csh } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -47,8 +33,6 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), - Self::Fish => input.to_owned(), - Self::Csh => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), } } @@ -169,7 +153,7 @@ impl ShellKind { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive + ShellKind::Posix | ShellKind::Nushell => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -200,14 +184,19 @@ pub struct ShellBuilder { kind: ShellKind, } +pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\""; + impl ShellBuilder { /// Create a new ShellBuilder as configured. - pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self { + pub fn new(is_local: bool, shell: &Shell) -> Self { let (program, args) = match shell { - Shell::System => match remote_system_shell { - Some(remote_shell) => (remote_shell.to_string(), Vec::new()), - None => (system_shell(), Vec::new()), - }, + Shell::System => { + if is_local { + (system_shell(), Vec::new()) + } else { + (DEFAULT_REMOTE_SHELL.to_string(), Vec::new()) + } + } Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }; @@ -223,7 +212,6 @@ impl ShellBuilder { self.interactive = false; self } - /// Returns the label to show in the terminal tab pub fn command_label(&self, command_label: &str) -> String { match self.kind { @@ -233,7 +221,7 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => { + ShellKind::Posix | ShellKind::Nushell => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); format!( "{} {interactivity}-c '$\"{}\"'", @@ -246,7 +234,7 @@ impl ShellBuilder { pub fn build( mut self, task_command: Option, - task_args: &[String], + task_args: &Vec, ) -> (String, Vec) { if let Some(task_command) = task_command { let combined_command = task_args.iter().fold(task_command, |mut command, arg| { @@ -270,11 +258,11 @@ mod test { #[test] fn test_nu_shell_variable_substitution() { let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(None, &shell); + let shell_builder = ShellBuilder::new(true, &shell); let (program, args) = shell_builder.build( Some("echo".into()), - &[ + &vec![ "${hello}".to_string(), "$world".to_string(), "nothing".to_string(), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index eb9e59f087..85e654eff4 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -22,7 +22,7 @@ pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{ShellBuilder, ShellKind}; +pub use shell_builder::{DEFAULT_REMOTE_SHELL, ShellBuilder}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/ui/src/utils/apca_contrast.rs b/crates/terminal_view/src/color_contrast.rs similarity index 100% rename from crates/ui/src/utils/apca_contrast.rs rename to crates/terminal_view/src/color_contrast.rs diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4..b93b267f58 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,11 +9,7 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -379,13 +375,9 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( +define_connection! { + pub static ref TERMINAL_DB: TerminalDb = + &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -422,8 +414,6 @@ impl Domain for TerminalDb { ]; } -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index fe3301fb89..c2fbeb7ee6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,3 +1,4 @@ +use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, @@ -26,7 +27,6 @@ use terminal::{ terminal_settings::TerminalSettings, }; use theme::{ActiveTheme, Theme, ThemeSettings}; -use ui::utils::ensure_minimum_contrast; use ui::{ParentElement, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -534,7 +534,7 @@ impl TerminalElement { // Only apply contrast adjustment to non-decorative characters if !Self::is_decorative_character(indexed.c) { - fg = ensure_minimum_contrast(fg, bg, minimum_contrast); + fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); } // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty @@ -1598,7 +1598,6 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: mod tests { use super::*; use gpui::{AbsoluteLength, Hsla, font}; - use ui::utils::apca_contrast; #[test] fn test_is_decorative_character() { @@ -1714,7 +1713,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); + let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs(); assert!( actual_contrast < 30.0, "White on light gray should have poor APCA contrast: {}", @@ -1722,12 +1721,12 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); assert!( adjusted.l < white_fg.l, "Adjusted color should be darker than original" ); - let adjusted_contrast = apca_contrast(adjusted, light_gray_bg).abs(); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 2: Dark colors (poor contrast) @@ -1745,7 +1744,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); + let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs(); assert!( actual_contrast < 30.0, "Black on dark gray should have poor APCA contrast: {}", @@ -1753,16 +1752,16 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); + let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); assert!( adjusted.l > black_fg.l, "Adjusted color should be lighter than original" ); - let adjusted_contrast = apca_contrast(adjusted, dark_gray_bg).abs(); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 3: Already good contrast - let good_contrast = ensure_minimum_contrast(black_fg, white_fg, 45.0); + let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0); assert_eq!( good_contrast, black_fg, "Good contrast should not be adjusted" @@ -1789,11 +1788,11 @@ mod tests { }; // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = ensure_minimum_contrast(white_fg, white_bg, 0.0); + let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0); assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); // With minimum APCA contrast of 15, it should adjust to a darker color - let adjusted = ensure_minimum_contrast(white_fg, white_bg, 15.0); + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0); assert!( adjusted.l < white_fg.l, "White on white should become darker, got l={}", @@ -1801,7 +1800,7 @@ mod tests { ); // Verify the contrast is now acceptable - let new_contrast = apca_contrast(adjusted, white_bg).abs(); + let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs(); assert!( new_contrast >= 15.0, "Adjusted APCA contrast {} should be >= 15.0", diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6b17911487..f40c4870f1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -481,17 +481,14 @@ impl TerminalPanel { window: &mut Window, cx: &mut Context, ) -> Task>> { - let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - ( - project.ssh_client().and_then(|it| it.read(cx).ssh_info()), - project.is_via_collab(), - ) - }) else { + let Ok(is_local) = self + .workspace + .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) + else { return Task::ready(Err(anyhow!("Project is not local"))); }; - let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell); + let builder = ShellBuilder::new(is_local, &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args); diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs deleted file mode 100644 index e20df7f001..0000000000 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ /dev/null @@ -1,825 +0,0 @@ -use super::{HoverTarget, HoveredWord, TerminalView}; -use anyhow::{Context as _, Result}; -use editor::Editor; -use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; -use itertools::Itertools; -use project::{Entry, Metadata}; -use std::path::PathBuf; -use terminal::PathLikeTarget; -use util::{ResultExt, debug_panic, paths::PathWithPosition}; -use workspace::{OpenOptions, OpenVisible, Workspace}; - -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry), - File(PathWithPosition, Metadata), -} - -impl OpenTarget { - fn is_file(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_file(), - OpenTarget::File(_, metadata) => !metadata.is_dir, - } - } - - fn is_dir(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_dir(), - OpenTarget::File(_, metadata) => metadata.is_dir, - } - } - - fn path(&self) -> &PathWithPosition { - match self { - OpenTarget::Worktree(path, _) => path, - OpenTarget::File(path, _) => path, - } - } -} - -pub(super) fn hover_path_like_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, -) -> Task<()> { - let file_to_open_task = possible_open_target(workspace, path_like_target, cx); - cx.spawn(async move |terminal_view, cx| { - let file_to_open = file_to_open_task.await; - terminal_view - .update(cx, |terminal_view, _| match file_to_open { - Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => { - terminal_view.hover = Some(HoverTarget { - tooltip: path.to_string(|path| path.to_string_lossy().to_string()), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }) -} - -fn possible_open_target( - workspace: &WeakEntity, - path_like_target: &PathLikeTarget, - cx: &App, -) -> Task> { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(None); - }; - // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. - // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. - let mut potential_paths = Vec::new(); - let cwd = path_like_target.terminal_dir.as_ref(); - let maybe_path = &path_like_target.maybe_path; - let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); - let path_with_position = PathWithPosition::parse_str(maybe_path); - let worktree_candidates = workspace - .read(cx) - .worktrees(cx) - .sorted_by_key(|worktree| { - let worktree_root = worktree.read(cx).abs_path(); - match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) { - Some(cwd_child) => cwd_child.components().count(), - None => usize::MAX, - } - }) - .collect::>(); - // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. - const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; - for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { - if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: original_path.row, - column: original_path.column, - }); - } - if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: path_with_position.row, - column: path_with_position.column, - }); - } - } - - let insert_both_paths = original_path != path_with_position; - potential_paths.insert(0, original_path); - if insert_both_paths { - potential_paths.insert(1, path_with_position); - } - - // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. - // That will be slow, though, so do the fast checks first. - let mut worktree_paths_to_check = Vec::new(); - for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - - for path_with_position in &potential_paths { - let path_to_check = if worktree_root.ends_with(&path_with_position.path) { - let root_path_with_position = PathWithPosition { - path: worktree_root.to_path_buf(), - row: path_with_position.row, - column: path_with_position.column, - }; - match worktree.read(cx).root_entry() { - Some(root_entry) => { - return Task::ready(Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - ))); - } - None => root_path_with_position, - } - } else { - PathWithPosition { - path: path_with_position - .path - .strip_prefix(&worktree_root) - .unwrap_or(&path_with_position.path) - .to_owned(), - row: path_with_position.row, - column: path_with_position.column, - } - }; - - if path_to_check.path.is_relative() - && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) - { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = - maybe_path - .strip_prefix("~") - .ok() - .and_then(|stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }) - { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - if let Some(cwd) = &cwd { - paths_to_check.push(PathWithPosition { - path: cwd.join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - for worktree in &worktree_candidates { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - paths_to_check - }) - .collect() - } else { - Vec::new() - }; - - let worktree_check_task = cx.spawn(async move |cx| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| { - let worktree_root = worktree.abs_path(); - let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - for entry in traversal { - if let Some(path_in_worktree) = worktree_paths_to_check - .iter() - .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }); - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - cx.background_spawn(async move { - for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() - && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - } - - worktree_check_task.await - }) -} - -pub(super) fn open_path_like_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) { - possibly_open_target(workspace, terminal_view, path_like_target, window, cx) - .detach_and_log_err(cx) -} - -fn possibly_open_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) -> Task>> { - if terminal_view.hover.is_none() { - return Task::ready(Ok(None)); - } - let workspace = workspace.clone(); - let path_like_target = path_like_target.clone(); - cx.spawn_in(window, async move |terminal_view, cx| { - let Some(open_target) = terminal_view - .update(cx, |_, cx| { - possible_open_target(&workspace, &path_like_target, cx) - })? - .await - else { - return Ok(None); - }; - - let path_to_open = open_target.path(); - let opened_items = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path_to_open.path.clone()], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .context("workspace update")? - .await; - if opened_items.len() != 1 { - debug_panic!( - "Received {} items for one path {path_to_open:?}", - opened_items.len(), - ); - } - - if let Some(opened_item) = opened_items.first() { - if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item { - if let Some(row) = path_to_open.row { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } - } - return Ok(Some(open_target)); - } - } else if open_target.is_dir() { - workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - return Ok(Some(open_target)); - } - } - Ok(None) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{Project, terminals::TerminalKind}; - use serde_json::json; - use std::path::{Path, PathBuf}; - use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; - use util::path; - use workspace::AppState; - - async fn init_test( - app_cx: &mut TestAppContext, - trees: impl IntoIterator, - worktree_roots: impl IntoIterator, - ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option, Option) - { - let fs = app_cx.update(AppState::test).fs.as_fake().clone(); - - app_cx.update(|cx| { - terminal::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - Project::init_settings(cx); - language::init(cx); - editor::init(cx); - }); - - for (path, tree) in trees { - fs.insert_tree(path, tree).await; - } - - let project = Project::test( - fs.clone(), - worktree_roots - .into_iter() - .map(Path::new) - .collect::>(), - app_cx, - ) - .await; - - let (workspace, cx) = - app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(None), cx) - }) - .await - .expect("Failed to create a terminal"); - - let workspace_a = workspace.clone(); - let (terminal_view, cx) = app_cx.add_window_view(|window, cx| { - TerminalView::new( - terminal, - workspace_a.downgrade(), - None, - project.downgrade(), - window, - cx, - ) - }); - - async move |hovered_word: HoveredWord, - path_like_target: PathLikeTarget| - -> (Option, Option) { - let workspace_a = workspace.clone(); - terminal_view - .update(cx, |_, cx| { - hover_path_like_target( - &workspace_a.downgrade(), - hovered_word, - &path_like_target, - cx, - ) - }) - .await; - - let hover_target = - terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone()); - - let open_target = terminal_view - .update_in(cx, |terminal_view, window, cx| { - possibly_open_target( - &workspace.downgrade(), - terminal_view, - &path_like_target, - window, - cx, - ) - }) - .await - .expect("Failed to possibly open target"); - - (hover_target, open_target) - } - } - - async fn test_path_like_simple( - test_path_like: &mut impl AsyncFnMut( - HoveredWord, - PathLikeTarget, - ) -> (Option, Option), - maybe_path: &str, - tooltip: &str, - terminal_dir: Option, - file: &str, - line: u32, - ) { - let (hover_target, open_target) = test_path_like( - HoveredWord { - word: maybe_path.to_string(), - word_match: AlacPoint::default()..=AlacPoint::default(), - id: 0, - }, - PathLikeTarget { - maybe_path: maybe_path.to_string(), - terminal_dir, - }, - ) - .await; - - let Some(hover_target) = hover_target else { - assert!( - hover_target.is_some(), - "Hover target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - hover_target.tooltip, tooltip, - "Tooltip mismatch at {file}:{line}:" - ); - assert_eq!( - hover_target.hovered_word.word, maybe_path, - "Hovered word mismatch at {file}:{line}:" - ); - - let Some(open_target) = open_target else { - assert!( - open_target.is_some(), - "Open target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - open_target.path().path, - Path::new(tooltip), - "Open target path mismatch at {file}:{line}:" - ); - } - - macro_rules! none_or_some { - () => { - None - }; - ($some:expr) => { - Some($some) - }; - } - - macro_rules! test_path_like { - ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => { - test_path_like_simple( - &mut $test_path_like, - path!($maybe_path), - path!($tooltip), - none_or_some!($($crate::PathBuf::from(path!($cwd)))?), - std::file!(), - std::line!(), - ) - .await - }; - } - - #[doc = "test_path_likes!(, , , { $(;)+ })"] - macro_rules! test_path_likes { - ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { - let mut test_path_like = init_test($cx, $trees, $worktrees).await; - #[doc ="test!(, , )"] - macro_rules! test { - ($maybe_path:literal, $tooltip:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd) - } - } - $($tests);+ - } } - } - - #[gpui::test] - async fn one_folder_worktree(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - }), - )], - vec![path!("/test")], - { - test!("lib.rs", "/test/lib.rs"); - test!("test.rs", "/test/test.rs"); - } - ) - } - - #[gpui::test] - async fn mixed_worktrees(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - "file.txt": "", - }), - ), - ], - vec![path!("/file.txt"), path!("/test")], - { - test!("file.txt", "/file.txt", "/"); - test!("lib.rs", "/test/lib.rs", "/test"); - test!("test.rs", "/test/test.rs", "/test"); - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - - #[gpui::test] - async fn worktree_file_preferred(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - - mod issues { - use super::*; - - // https://github.com/zed-industries/zed/issues/28407 - #[gpui::test] - async fn issue_28407_siblings(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/dir1"), - json!({ - "dir 2": { - "C.py": "" - }, - "dir 3": { - "C.py": "" - }, - }), - )], - vec![path!("/dir1")], - { - test!("C.py", "/dir1/dir 2/C.py", "/dir1"); - test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); - test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28407 - // See https://github.com/zed-industries/zed/issues/34027 - // See https://github.com/zed-industries/zed/issues/33498 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_28407_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - // Failing currently - test!("main.rs", "/project/src/main.rs", "/project"); - test!("main.rs", "/project/src/main.rs", "/project/src"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); - - test!("src/main.rs", "/project/src/main.rs", "/project"); - test!("src/main.rs", "/project/src/main.rs", "/project/src"); - // Failing currently - test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); - // Failing currently - test!( - "src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - - test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/src" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - #[gpui::test] - async fn issue_28339(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp"), - json!({ - "issue28339": { - "foo": { - "bar.txt": "" - }, - }, - }), - )], - vec![path!("/tmp")], - { - test!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - test!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp/issue34027"), - json!({ - "test.txt": "", - "foo": { - "test.txt": "", - } - }), - ),], - vec![path!("/tmp/issue34027")], - { - test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027"); - test!( - "test.txt", - "/tmp/issue34027/foo/test.txt", - "/tmp/issue34027/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - test!("file.txt", "/file.txt", "/"); - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - } -} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7..5b4d327140 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,21 +1,22 @@ +mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; -mod terminal_path_like_target; pub mod terminal_scrollbar; mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; +use itertools::Itertools; use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Entry, Metadata, Project, search::SearchQuery, terminals::TerminalKind}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -30,17 +31,16 @@ use terminal::{ }; use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; -use terminal_path_like_target::{hover_path_like_target, open_path_like_target}; use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, }; -use util::ResultExt; +use util::{ResultExt, debug_panic, paths::PathWithPosition}; use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, - delete_unloaded_items, + CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation, + Workspace, WorkspaceId, delete_unloaded_items, item::{ BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, }, @@ -48,6 +48,7 @@ use workspace::{ searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, }; +use anyhow::Context as _; use serde::Deserialize; use settings::{Settings, SettingsStore}; use smol::Timer; @@ -63,6 +64,7 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view @@ -179,7 +181,6 @@ impl ContentMode { } #[derive(Debug)] -#[cfg_attr(test, derive(Clone, Eq, PartialEq))] struct HoverTarget { tooltip: String, hovered_word: HoveredWord, @@ -1065,13 +1066,37 @@ fn subscribe_for_terminal_events( .as_ref() .map(|hover| &hover.hovered_word) { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = hover_path_like_target( + let valid_files_to_open_task = possible_open_target( &workspace, - hovered_word.clone(), - path_like_target, + &path_like_target.terminal_dir, + &path_like_target.maybe_path, cx, ); + let hovered_word = hovered_word.clone(); + + terminal_view.hover = None; + terminal_view.hover_tooltip_update = + cx.spawn(async move |terminal_view, cx| { + let file_to_open = valid_files_to_open_task.await; + terminal_view + .update(cx, |terminal_view, _| match file_to_open { + Some( + OpenTarget::File(path, _) + | OpenTarget::Worktree(path, _), + ) => { + terminal_view.hover = Some(HoverTarget { + tooltip: path.to_string(|path| { + path.to_string_lossy().to_string() + }), + hovered_word, + }); + } + None => { + terminal_view.hover = None; + } + }) + .ok(); + }); cx.notify(); } } @@ -1085,13 +1110,86 @@ fn subscribe_for_terminal_events( Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.open_url(url), - MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target( - &workspace, - terminal_view, - path_like_target, - window, - cx, - ), + + MaybeNavigationTarget::PathLike(path_like_target) => { + if terminal_view.hover.is_none() { + return; + } + let task_workspace = workspace.clone(); + let path_like_target = path_like_target.clone(); + cx.spawn_in(window, async move |terminal_view, cx| { + let open_target = terminal_view + .update(cx, |_, cx| { + possible_open_target( + &task_workspace, + &path_like_target.terminal_dir, + &path_like_target.maybe_path, + cx, + ) + })? + .await; + if let Some(open_target) = open_target { + let path_to_open = open_target.path(); + let opened_items = task_workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path_to_open.path.clone()], + OpenOptions { + visible: Some(OpenVisible::OnlyDirectories), + ..Default::default() + }, + None, + window, + cx, + ) + }) + .context("workspace update")? + .await; + if opened_items.len() != 1 { + debug_panic!( + "Received {} items for one path {path_to_open:?}", + opened_items.len(), + ); + } + + if let Some(opened_item) = opened_items.first() { + if open_target.is_file() { + if let Some(Ok(opened_item)) = opened_item + && let Some(row) = path_to_open.row + { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = + opened_item.downcast::() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); + } + } + } else if open_target.is_dir() { + task_workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActivateProjectPanel); + }) + })?; + } + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } }, Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), @@ -1105,6 +1203,241 @@ fn subscribe_for_terminal_events( vec![terminal_subscription, terminal_events_subscription] } +#[derive(Debug, Clone)] +enum OpenTarget { + Worktree(PathWithPosition, Entry), + File(PathWithPosition, Metadata), +} + +impl OpenTarget { + fn is_file(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_file(), + OpenTarget::File(_, metadata) => !metadata.is_dir, + } + } + + fn is_dir(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_dir(), + OpenTarget::File(_, metadata) => metadata.is_dir, + } + } + + fn path(&self) -> &PathWithPosition { + match self { + OpenTarget::Worktree(path, _) => path, + OpenTarget::File(path, _) => path, + } + } +} + +fn possible_open_target( + workspace: &WeakEntity, + cwd: &Option, + maybe_path: &str, + cx: &App, +) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(None); + }; + // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. + // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. + let mut potential_paths = Vec::new(); + let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); + let path_with_position = PathWithPosition::parse_str(maybe_path); + let worktree_candidates = workspace + .read(cx) + .worktrees(cx) + .sorted_by_key(|worktree| { + let worktree_root = worktree.read(cx).abs_path(); + match cwd + .as_ref() + .and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) + { + Some(cwd_child) => cwd_child.components().count(), + None => usize::MAX, + } + }) + .collect::>(); + // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. + for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { + if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: original_path.row, + column: original_path.column, + }); + } + if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: path_with_position.row, + column: path_with_position.column, + }); + } + } + + let insert_both_paths = original_path != path_with_position; + potential_paths.insert(0, original_path); + if insert_both_paths { + potential_paths.insert(1, path_with_position); + } + + // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. + // That will be slow, though, so do the fast checks first. + let mut worktree_paths_to_check = Vec::new(); + for worktree in &worktree_candidates { + let worktree_root = worktree.read(cx).abs_path(); + let mut paths_to_check = Vec::with_capacity(potential_paths.len()); + + for path_with_position in &potential_paths { + let path_to_check = if worktree_root.ends_with(&path_with_position.path) { + let root_path_with_position = PathWithPosition { + path: worktree_root.to_path_buf(), + row: path_with_position.row, + column: path_with_position.column, + }; + match worktree.read(cx).root_entry() { + Some(root_entry) => { + return Task::ready(Some(OpenTarget::Worktree( + root_path_with_position, + root_entry.clone(), + ))); + } + None => root_path_with_position, + } + } else { + PathWithPosition { + path: path_with_position + .path + .strip_prefix(&worktree_root) + .unwrap_or(&path_with_position.path) + .to_owned(), + row: path_with_position.row, + column: path_with_position.column, + } + }; + + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); + } + + paths_to_check.push(path_to_check); + } + + if !paths_to_check.is_empty() { + worktree_paths_to_check.push((worktree.clone(), paths_to_check)); + } + } + + // Before entire worktree traversal(s), make an attempt to do FS checks if available. + let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { + potential_paths + .into_iter() + .flat_map(|path_to_check| { + let mut paths_to_check = Vec::new(); + let maybe_path = &path_to_check.path; + if maybe_path.starts_with("~") { + if let Some(home_path) = + maybe_path + .strip_prefix("~") + .ok() + .and_then(|stripped_maybe_path| { + Some(dirs::home_dir()?.join(stripped_maybe_path)) + }) + { + paths_to_check.push(PathWithPosition { + path: home_path, + row: path_to_check.row, + column: path_to_check.column, + }); + } + } else { + paths_to_check.push(PathWithPosition { + path: maybe_path.clone(), + row: path_to_check.row, + column: path_to_check.column, + }); + if maybe_path.is_relative() { + if let Some(cwd) = &cwd { + paths_to_check.push(PathWithPosition { + path: cwd.join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + for worktree in &worktree_candidates { + paths_to_check.push(PathWithPosition { + path: worktree.read(cx).abs_path().join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + } + } + paths_to_check + }) + .collect() + } else { + Vec::new() + }; + + let worktree_check_task = cx.spawn(async move |cx| { + for (worktree, worktree_paths_to_check) in worktree_paths_to_check { + let found_entry = worktree + .update(cx, |worktree, _| { + let worktree_root = worktree.abs_path(); + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { + if let Some(path_in_worktree) = worktree_paths_to_check + .iter() + .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) + { + return Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_in_worktree.row, + column: path_in_worktree.column, + }, + entry.clone(), + )); + } + } + None + }) + .ok()?; + if let Some(found_entry) = found_entry { + return Some(found_entry); + } + } + None + }); + + let fs = workspace.read(cx).project().read(cx).fs().clone(); + cx.background_spawn(async move { + for mut path_to_check in fs_paths_to_check { + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); + } + } + + worktree_check_task.await + }) +} + fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let str = query.as_str(); if query.is_regex() { diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 1c28942490..ed43c5277a 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -119,7 +119,7 @@ impl Render for OnboardingBanner { h_flex() .h_full() .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) + .child(Icon::new(self.details.icon_name).size(IconSize::Small)) .child( h_flex() .gap_0p5() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ad64dac9c6..b84a2800b6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -275,11 +275,11 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "ACP Onboarding", - IconName::Sparkle, - "Bring Your Own Agent", - Some("Introducing:".into()), - zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), + "Debugger Onboarding", + IconName::Debug, + "The Debugger", + None, + zed_actions::debugger::OpenOnboardingModal.boxed_clone(), cx, ) }); diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index b1ead18ee7..7ffeda881c 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -132,7 +132,6 @@ impl RenderOnce for Callout { h_flex() .min_w_0() - .w_full() .p_2() .gap_2() .items_start() diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 21ab283d88..25575c4f1e 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -561,7 +561,7 @@ impl ContextMenu { action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), - icon_size: IconSize::XSmall, + icon_size: IconSize::Small, icon_position: IconPosition::End, icon_color: None, disabled: false, diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 4bb3419176..98406cd1e2 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{ClickEvent, CursorStyle, SharedString}; +use gpui::{ClickEvent, CursorStyle}; use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*}; @@ -14,7 +14,6 @@ pub struct Disclosure { cursor_style: CursorStyle, opened_icon: IconName, closed_icon: IconName, - visible_on_hover: Option, } impl Disclosure { @@ -28,7 +27,6 @@ impl Disclosure { cursor_style: CursorStyle::PointingHand, opened_icon: IconName::ChevronDown, closed_icon: IconName::ChevronRight, - visible_on_hover: None, } } @@ -75,13 +73,6 @@ impl Clickable for Disclosure { } } -impl VisibleOnHover for Disclosure { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.visible_on_hover = Some(group_name.into()); - self - } -} - impl RenderOnce for Disclosure { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { IconButton::new( @@ -96,9 +87,6 @@ impl RenderOnce for Disclosure { .icon_size(IconSize::Small) .disabled(self.disabled) .toggle_state(self.selected) - .when_some(self.visible_on_hover.clone(), |this, group_name| { - this.visible_on_hover(group_name) - }) .when_some(self.on_toggle, move |this, on_toggle| { this.on_click(move |event, window, cx| on_toggle(event, window, cx)) }) diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 6e552ddcee..09c3bbeb94 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -13,9 +13,6 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { - AcpGrid, - AcpLogo, - AcpLogoSerif, AiGrid, DebuggerGrid, Grid, diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 81817045dc..1e7bb40c40 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,8 +1,8 @@ use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, + Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, + relative, }; use itertools::Itertools; @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub keystrokes: Vec, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -59,7 +59,7 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { keystrokes, platform_style: PlatformStyle::platform(), @@ -99,16 +99,16 @@ impl KeyBinding { } fn render_key( - key: &str, + keystroke: &Keystroke, color: Option, platform_style: PlatformStyle, size: impl Into>, ) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); + let key_icon = icon_for_key(keystroke, platform_style); match key_icon { Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), None => { - let key = util::capitalize(key); + let key = util::capitalize(&keystroke.key); Key::new(&key, color).size(size).into_any_element() } } @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.display_key.to_string()) + .map(|k| k.key.to_string()) .collect::>() .join(" ") ) @@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding { .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( + .children(render_keystroke( keystroke, color, self.size, @@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding { } } -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, +pub fn render_keystroke( + keystroke: &Keystroke, color: Option, size: impl Into>, platform_style: PlatformStyle, @@ -163,39 +163,26 @@ pub fn render_keybinding_keystroke( let size = size.into(); if use_text { - let element = Key::new( - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) + .size(size) + .into_any_element(); vec![element] } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.display_modifiers, + &keystroke.modifiers, platform_style, color, size, true, )); - elements.push(render_key( - &keystroke.display_key, - color, - platform_style, - size, - )); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { +fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { + match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), "up" => Some(IconName::ArrowUp), @@ -392,7 +379,7 @@ impl KeyIcon { /// Returns a textual representation of the key binding for the given [`Action`]. pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) + Some(text_for_keystrokes(key_binding.keystrokes(), cx)) } pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { @@ -400,50 +387,22 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { let vim_enabled = cx.try_global::().is_some(); keystrokes .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) + .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .join(" ") } -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { +pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { let platform_style = PlatformStyle::platform(); let vim_enabled = cx.try_global::().is_some(); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = cx.try_global::().is_some(); - keystroke_text(modifiers, key, platform_style, vim_enabled) + keystroke_text(keystroke, platform_style, vim_enabled) } /// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { +fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { let mut text = String::new(); let delimiter = '-'; - if modifiers.function { + if keystroke.modifiers.function { match vim_mode { false => text.push_str("Fn"), true => text.push_str("fn"), @@ -452,7 +411,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.control { + if keystroke.modifiers.control { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), @@ -462,7 +421,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.platform { + if keystroke.modifiers.platform { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, true) => text.push_str("cmd"), @@ -475,7 +434,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.alt { + if keystroke.modifiers.alt { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), @@ -485,7 +444,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.shift { + if keystroke.modifiers.shift { match (platform_style, vim_mode) { (_, false) => text.push_str("Shift"), (_, true) => text.push_str("shift"), @@ -494,9 +453,9 @@ fn keystroke_text( } if vim_mode { - text.push_str(key) + text.push_str(&keystroke.key) } else { - let key = match key { + let key = match keystroke.key.as_str() { "pageup" => "PageUp", "pagedown" => "PageDown", key => &util::capitalize(key), @@ -603,11 +562,9 @@ mod tests { #[test] fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac, false ), @@ -615,8 +572,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux, false ), @@ -624,19 +580,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows, false ), "Win-C".to_string() ); - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Mac, false ), @@ -644,8 +597,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Linux, false ), @@ -653,19 +605,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Windows, false ), "Ctrl-Alt-Delete".to_string() ); - let keystroke = Keystroke::parse("shift-pageup").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Mac, false ), @@ -673,8 +622,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Linux, false, ), @@ -682,8 +630,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Windows, false ), diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs index dc830559ca..8c9ea62424 100644 --- a/crates/ui/src/components/label.rs +++ b/crates/ui/src/components/label.rs @@ -2,10 +2,8 @@ mod highlighted_label; mod label; mod label_like; mod loading_label; -mod spinner_label; pub use highlighted_label::*; pub use label::*; pub use label_like::*; pub use loading_label::*; -pub use spinner_label::*; diff --git a/crates/ui/src/components/label/spinner_label.rs b/crates/ui/src/components/label/spinner_label.rs deleted file mode 100644 index b7b65fbcc9..0000000000 --- a/crates/ui/src/components/label/spinner_label.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::prelude::*; -use gpui::{Animation, AnimationExt, FontWeight}; -use std::time::Duration; - -/// Different types of spinner animations -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub enum SpinnerVariant { - #[default] - Dots, - DotsVariant, -} - -/// A spinner indication, based on the label component, that loops through -/// frames of the specified animation. It implements `LabelCommon` as well. -/// -/// # Default Example -/// -/// ``` -/// use ui::{SpinnerLabel}; -/// -/// SpinnerLabel::new(); -/// ``` -/// -/// # Variant Example -/// -/// ``` -/// use ui::{SpinnerLabel}; -/// -/// SpinnerLabel::dots_variant(); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct SpinnerLabel { - base: Label, - variant: SpinnerVariant, - frames: Vec<&'static str>, - duration: Duration, -} - -impl SpinnerVariant { - fn frames(&self) -> Vec<&'static str> { - match self { - SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], - SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"], - } - } - - fn duration(&self) -> Duration { - match self { - SpinnerVariant::Dots => Duration::from_millis(1000), - SpinnerVariant::DotsVariant => Duration::from_millis(1000), - } - } - - fn animation_id(&self) -> &'static str { - match self { - SpinnerVariant::Dots => "spinner_label_dots", - SpinnerVariant::DotsVariant => "spinner_label_dots_variant", - } - } -} - -impl SpinnerLabel { - pub fn new() -> Self { - Self::with_variant(SpinnerVariant::default()) - } - - pub fn with_variant(variant: SpinnerVariant) -> Self { - let frames = variant.frames(); - let duration = variant.duration(); - - SpinnerLabel { - base: Label::new(frames[0]), - variant, - frames, - duration, - } - } - - pub fn dots() -> Self { - Self::with_variant(SpinnerVariant::Dots) - } - - pub fn dots_variant() -> Self { - Self::with_variant(SpinnerVariant::DotsVariant) - } -} - -impl LabelCommon for SpinnerLabel { - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.base = self.base.alpha(alpha); - self - } - - fn underline(mut self) -> Self { - self.base = self.base.underline(); - self - } - - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -impl RenderOnce for SpinnerLabel { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let frames = self.frames.clone(); - let duration = self.duration; - - self.base.color(Color::Muted).with_animation( - self.variant.animation_id(), - Animation::new(duration).repeat(), - move |mut label, delta| { - let frame_index = (delta * frames.len() as f32) as usize % frames.len(); - - label.set_text(frames[frame_index]); - label - }, - ) - } -} - -impl Component for SpinnerLabel { - fn scope() -> ComponentScope { - ComponentScope::Loading - } - - fn name() -> &'static str { - "Spinner Label" - } - - fn sort_name() -> &'static str { - "Spinner Label" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let examples = vec![ - single_example("Default", SpinnerLabel::new().into_any_element()), - single_example( - "Dots Variant", - SpinnerLabel::dots_variant().into_any_element(), - ), - ]; - - Some(example_group(examples).vertical().into_any_element()) - } -} diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index cd7d8eb497..26a59001f6 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -3,14 +3,12 @@ use gpui::App; use theme::ActiveTheme; -mod apca_contrast; mod color_contrast; mod corner_solver; mod format_distance; mod search_input; mod with_rem_size; -pub use apca_contrast::*; pub use color_contrast::*; pub use corner_solver::{CornerSolver, inner_corner_radius}; pub use format_distance::*; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 1192b14812..b430120314 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -166,7 +166,7 @@ impl> From for SanitizedPath { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy)] pub enum PathStyle { Posix, Windows, diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d..2bc531268d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,8 +23,6 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, ] ); @@ -33,7 +31,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); - Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -433,15 +430,6 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } - - pub fn helix_goto_last_modification( - &mut self, - _: &HelixGotoLastModification, - window: &mut Window, - cx: &mut Context, - ) { - self.jump(".".into(), false, false, window, cx); - } } #[cfg(test)] @@ -453,7 +441,6 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // « // ˇ // » @@ -515,7 +502,6 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test delete a selection cx.set_state( @@ -596,7 +582,6 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" @@ -650,7 +635,6 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -668,7 +652,6 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -691,7 +674,6 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -734,7 +716,6 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -782,72 +763,4 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } - #[gpui::test] - async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // First copy some text to clipboard - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test paste with shift-r on single cursor - cx.set_state("foo ˇbar", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); - - // Test paste with shift-r on selection - cx.set_state("foo «barˇ» baz", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("ˇhello", Mode::HelixNormal); - assert_eq!(cx.mode(), Mode::HelixNormal); - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("escape"); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("m o d i f i e d space"); - cx.simulate_keystrokes("escape"); - - // TODO: this fails, because state is no longer helix - cx.assert_state( - "line one\nline modified ˇtwo\nline three", - Mode::HelixNormal, - ); - - // Move cursor away from the modification - cx.simulate_keystrokes("up"); - - // Use "g ." to go back to last modification - cx.simulate_keystrokes("g ."); - - // Verify we're back at the modification location and still in HelixNormal mode - cx.assert_state( - "line one\nline modifiedˇ two\nline three", - Mode::HelixNormal, - ); - } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a54d3caa60..a2f165e9fe 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1610,20 +1610,10 @@ fn up_down_buffer_rows( map.line_len(begin_folded_line.row()) }; - let point = DisplayPoint::new(begin_folded_line.row(), new_col); - let mut clipped_point = map.clip_point(point, bias); - - // When navigating vertically in vim mode with inlay hints present, - // we need to handle the case where clipping moves us to a different row. - // This can happen when moving down (Bias::Right) and hitting an inlay hint. - // Re-clip with opposite bias to stay on the intended line. - // - // See: https://github.com/zed-industries/zed/issues/29134 - if clipped_point.row() > point.row() { - clipped_point = map.clip_point(point, Bias::Left); - } - - (clipped_point, goal) + ( + map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias), + goal, + ) } fn down_display( @@ -3852,84 +3842,6 @@ mod test { ); } - #[gpui::test] - async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Test the exact scenario from issue #29134 - cx.set_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_oneˇ = this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Normal, - ); - - // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name") - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - // The empty line is at line 3 (0-indexed) - let line_start = snapshot.anchor_after(Point::new(3, 0)); - let inlay_text = ": Vec"; - let inlay = Inlay::edit_prediction(1, line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode - cx.simulate_keystrokes("v"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« ˇ»= this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Move down - should go to the beginning of line 4, not skip to line 5 - cx.simulate_keystrokes("j"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« = this_is_a_long_name - ˇ» .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Test with multiple movements - cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal); - - // Add type hint on the empty line - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let empty_line_start = snapshot.anchor_after(Point::new(2, 0)); - let inlay_text = ": i32"; - let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode and move down twice - cx.simulate_keystrokes("v j j"); - cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual); - } - #[gpui::test] async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 34ac4aab1f..1d2a4e9b61 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -70,19 +70,8 @@ impl Vim { } else { Point::new(row, 0) }; - let end = if row == selection.end.row { - selection.end - } else { - Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) - }; - let number_result = if !selection.is_empty() { - find_number_in_range(&snapshot, start, end) - } else { - find_number(&snapshot, start) - }; - - if let Some((range, num, radix)) = number_result { + if let Some((range, num, radix)) = find_number(&snapshot, start) { let replace = match radix { 10 => increment_decimal_string(&num, delta), 16 => increment_hex_string(&num, delta), @@ -200,90 +189,6 @@ fn increment_binary_string(num: &str, delta: i64) -> String { format!("{:0width$b}", result, width = num.len()) } -fn find_number_in_range( - snapshot: &MultiBufferSnapshot, - start: Point, - end: Point, -) -> Option<(Range, String, u32)> { - let start_offset = start.to_offset(snapshot); - let end_offset = end.to_offset(snapshot); - - let mut offset = start_offset; - - // Backward scan to find the start of the number, but stop at start_offset - for ch in snapshot.reversed_chars_at(offset) { - if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { - if offset == 0 { - break; - } - offset -= ch.len_utf8(); - if offset < start_offset { - offset = start_offset; - break; - } - } else { - break; - } - } - - let mut begin = None; - let mut end_num = None; - let mut num = String::new(); - let mut radix = 10; - - let mut chars = snapshot.chars_at(offset).peekable(); - - while let Some(ch) = chars.next() { - if offset >= end_offset { - break; // stop at end of selection - } - - if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) { - radix = 2; - begin = None; - num = String::new(); - } else if num == "0" - && ch == 'x' - && chars.peek().is_some() - && chars.peek().unwrap().is_ascii_hexdigit() - { - radix = 16; - begin = None; - num = String::new(); - } - - if ch.is_digit(radix) - || (begin.is_none() - && ch == '-' - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(radix)) - { - if begin.is_none() { - begin = Some(offset); - } - num.push(ch); - } else if begin.is_some() { - end_num = Some(offset); - break; - } else if ch == '\n' { - break; - } - - offset += ch.len_utf8(); - } - - if let Some(begin) = begin { - let end_num = end_num.unwrap_or(offset); - Some(( - begin.to_point(snapshot)..end_num.to_point(snapshot), - num, - radix, - )) - } else { - None - } -} - fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -859,18 +764,4 @@ mod test { cx.simulate_keystrokes("v b ctrl-a"); cx.assert_state("let enabled = ˇOff;", Mode::Normal); } - - #[gpui::test] - async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ123").await; - cx.simulate_shared_keystrokes("v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); - cx.simulate_shared_keystrokes("l v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); - cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") - .await; - cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); - } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index dba003ec5f..4fbeec7236 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -203,10 +203,7 @@ impl Vim { // hook into the existing to clear out any vim search state on cmd+f or edit -> find. fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { - // Preserve the current mode when resetting search state - let current_mode = self.mode; self.search = Default::default(); - self.search.prior_mode = current_mode; cx.propagate(); } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index fe4bc7433d..c0176cb12c 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,10 +7,8 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::define_connection; +use db::sqlez_macros::sql; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1670,12 +1668,8 @@ impl MarksView { } } -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ +define_connection! ( + pub static ref DB: VimDb = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1695,9 +1689,7 @@ impl Domain for VimDb { ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); +); struct SerializedMark { path: Arc, diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json deleted file mode 100644 index ebb4eece78..0000000000 --- a/crates/vim/test_data/test_increment_visual_partial_number.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"ˇ123"}} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ133","mode":"Normal"}} -{"Key":"l"} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"1ˇ34","mode":"Normal"}} -{"Key":"shift-v"} -{"Key":"y"} -{"Key":"p"} -{"Key":"p"} -{"Key":"ctrl-v"} -{"Key":"k"} -{"Key":"k"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}} diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index 71dab74820..f0ed5b4a18 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -162,19 +162,6 @@ impl Receiver { pending_waker_id: None, } } - - /// Creates a new [`Receiver`] holding an initial value that will never change. - pub fn constant(value: T) -> Self { - let state = Arc::new(RwLock::new(State { - value, - wakers: BTreeMap::new(), - next_waker_id: WakerId::default(), - version: 0, - closed: false, - })); - - Self { state, version: 0 } - } } impl Receiver { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322e..e1bda7ad36 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,6 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true +bincode = "1.2.1" call.workspace = true client.workspace = true clock.workspace = true @@ -79,6 +80,5 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true tempfile.workspace = true zlog.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 149a122c0c..7a8de6e910 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -915,11 +915,6 @@ impl Render for PanelButtons { .on_click({ let action = action.boxed_clone(); move |_, window, cx| { - telemetry::event!( - "Panel Button Clicked", - name = name, - toggle_state = !is_open - ); window.focus(&focus_handle); window.dispatch_action(action.boxed_clone(), cx) } diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index f68b58ff82..a8387369f4 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -5,9 +5,7 @@ use smallvec::SmallVec; use ui::App; use util::{ResultExt, paths::PathExt}; -use crate::{ - NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, -}; +use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); @@ -46,13 +44,7 @@ impl HistoryManager { .unwrap_or_default() .into_iter() .rev() - .filter_map(|(id, location, paths)| { - if matches!(location, SerializedWorkspaceLocation::Local) { - Some(HistoryManagerEntry::new(id, &paths)) - } else { - None - } - }) + .map(|(id, location)| HistoryManagerEntry::new(id, &location)) .collect::>(); this.update(cx, |this, cx| { this.history = recent_folders; @@ -126,9 +118,9 @@ impl HistoryManager { } impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, paths: &PathList) -> Self { - let path = paths - .paths() + pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { + let path = location + .sorted_paths() .iter() .map(|path| path.compact()) .collect::>(); diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs deleted file mode 100644 index b8c0db29d3..0000000000 --- a/crates/workspace/src/invalid_buffer_view.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use gpui::{EventEmitter, FocusHandle, Focusable}; -use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, -}; -use zed_actions::workspace::OpenWithSystem; - -use crate::Item; - -/// A view to display when a certain buffer fails to open. -pub struct InvalidBufferView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the buffer. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidBufferView { - pub fn new( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - _: &mut Window, - cx: &mut App, - ) -> Self { - Self { - is_local, - abs_path: Arc::from(abs_path), - error: format!("{e}").into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidBufferView { - type Event = (); - - fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { - // Ensure we always render at least the filename. - detail += 1; - - let path = self.abs_path.as_ref(); - - let mut prefix = path; - while detail > 0 { - if let Some(parent) = prefix.parent() { - prefix = parent; - detail -= 1; - } else { - break; - } - } - - let path = if detail > 0 { - path - } else { - path.strip_prefix(prefix).unwrap_or(path) - }; - - SharedString::new(path.to_string_lossy()) - } -} - -impl EventEmitter<()> for InvalidBufferView {} - -impl Focusable for InvalidBufferView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidBufferView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let abs_path = self.abs_path.clone(); - v_flex() - .size_full() - .track_focus(&self.focus_handle(cx)) - .flex_none() - .justify_center() - .overflow_hidden() - .key_context("InvalidBuffer") - .child( - h_flex().size_full().justify_center().child( - v_flex() - .justify_center() - .gap_2() - .child(h_flex().justify_center().child("Unsupported file type")) - .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), - ), - ) - }), - ), - ) - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b9..5a497398f9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,7 +1,6 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -23,7 +22,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::Path, rc::Rc, sync::Arc, time::Duration, @@ -1163,22 +1161,6 @@ pub trait ProjectItem: Item { ) -> Self where Self: Sized; - - /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, - /// with the error from that failure as an argument. - /// Allows to open an item that can gracefully display and handle errors. - fn for_broken_project_item( - _abs_path: &Path, - _is_local: bool, - _e: &anyhow::Error, - _window: &mut Window, - _cx: &mut App, - ) -> Option - where - Self: Sized, - { - None - } } #[derive(Debug)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe8014d9f7..23c8c0b185 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,6 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, - invalid_buffer_view::InvalidBufferView, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, @@ -514,7 +513,7 @@ impl Pane { } } - fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { + fn alternate_file(&mut self, window: &mut Window, cx: &mut Context) { let (_, alternative) = &self.alternate_file_items; if let Some(alternative) = alternative { let existing = self @@ -788,7 +787,7 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } - pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { + pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -799,7 +798,7 @@ impl Pane { } } - fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { + fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -898,43 +897,19 @@ impl Pane { } } } - - let set_up_existing_item = - |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { - // If the item is already open, and the item is a preview item - // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = pane.preview_item_id - && let Some(tab) = pane.items.get(index) - && tab.item_id() == preview_item_id - && !allow_preview - { - pane.set_preview_item_id(None, cx); - } - if activate { - pane.activate_item(index, focus_item, focus_item, window, cx); - } - }; - let set_up_new_item = |new_item: Box, - destination_index: Option, - pane: &mut Self, - window: &mut Window, - cx: &mut Context| { - if allow_preview { - pane.set_preview_item_id(Some(new_item.item_id()), cx); - } - pane.add_item_inner( - new_item, - true, - focus_item, - activate, - destination_index, - window, - cx, - ); - }; - if let Some((index, existing_item)) = existing_item { - set_up_existing_item(index, self, window, cx); + // If the item is already open, and the item is a preview item + // and we are not allowing items to open as preview, mark the item as persistent. + if let Some(preview_item_id) = self.preview_item_id + && let Some(tab) = self.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + self.set_preview_item_id(None, cx); + } + if activate { + self.activate_item(index, focus_item, focus_item, window, cx); + } existing_item } else { // If the item is being opened as preview and we have an existing preview tab, @@ -946,46 +921,21 @@ impl Pane { }; let new_item = build_item(self, window, cx); - // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { - let mut already_open_view = None; - let mut views_to_close = HashSet::default(); - for existing_error_view in self - .items_of_type::() - .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) - { - if already_open_view.is_none() - && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error - { - already_open_view = Some(existing_error_view); - } else { - views_to_close.insert(existing_error_view.item_id()); - } - } - let resulting_item = match already_open_view { - Some(already_open_view) => { - if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { - set_up_existing_item(index, self, window, cx); - } - Box::new(already_open_view) as Box<_> - } - None => { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item - } - }; - - self.close_items(window, cx, SaveIntent::Skip, |existing_item| { - views_to_close.contains(&existing_item) - }) - .detach(); - - resulting_item - } else { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item + if allow_preview { + self.set_preview_item_id(Some(new_item.item_id()), cx); } + self.add_item_inner( + new_item.clone(), + true, + focus_item, + activate, + destination_index, + window, + cx, + ); + + new_item } } @@ -1283,9 +1233,9 @@ impl Pane { } } - pub fn activate_previous_item( + pub fn activate_prev_item( &mut self, - _: &ActivatePreviousItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1295,12 +1245,12 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } pub fn activate_next_item( &mut self, - _: &ActivateNextItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1310,15 +1260,10 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } - pub fn swap_item_left( - &mut self, - _: &SwapItemLeft, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; if index == 0 { return; @@ -1328,14 +1273,9 @@ impl Pane { self.activate_item(index - 1, true, true, window, cx); } - pub fn swap_item_right( - &mut self, - _: &SwapItemRight, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; - if index + 1 >= self.items.len() { + if index + 1 == self.items.len() { return; } @@ -1343,16 +1283,6 @@ impl Pane { self.activate_item(index + 1, true, true, window, cx); } - pub fn activate_last_item( - &mut self, - _: &ActivateLastItem, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.items.len().saturating_sub(1); - self.activate_item(index, true, true, window, cx); - } - pub fn close_active_item( &mut self, action: &CloseActiveItem, @@ -2901,9 +2831,7 @@ impl Pane { .on_click({ let entity = cx.entity(); move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx) - }) + entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) } }) .disabled(!self.can_navigate_backward()) @@ -2918,11 +2846,7 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_forward(&Default::default(), window, cx) - }) - } + move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) }) .disabled(!self.can_navigate_forward()) .tooltip({ @@ -3554,6 +3478,9 @@ impl Render for Pane { .size_full() .flex_none() .overflow_hidden() + .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| { + pane.alternate_file(window, cx); + })) .on_action( cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), ) @@ -3570,6 +3497,12 @@ impl Render for Pane { .on_action( cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), ) + .on_action( + cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)), + ) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); })) @@ -3577,8 +3510,6 @@ impl Render for Pane { cx.emit(Event::JoinAll); })) .on_action(cx.listener(Pane::toggle_zoom)) - .on_action(cx.listener(Self::navigate_backward)) - .on_action(cx.listener(Self::navigate_forward)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -3590,14 +3521,33 @@ impl Render for Pane { ); }), ) - .on_action(cx.listener(Self::alternate_file)) - .on_action(cx.listener(Self::activate_last_item)) - .on_action(cx.listener(Self::activate_previous_item)) - .on_action(cx.listener(Self::activate_next_item)) - .on_action(cx.listener(Self::swap_item_left)) - .on_action(cx.listener(Self::swap_item_right)) - .on_action(cx.listener(Self::toggle_pin_tab)) - .on_action(cx.listener(Self::unpin_all_tabs)) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| { + pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| { + pane.activate_prev_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| { + pane.activate_next_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)), + ) + .on_action(cx.listener(|pane, action, window, cx| { + pane.toggle_pin_tab(action, window, cx); + })) + .on_action(cx.listener(|pane, action, window, cx| { + pane.unpin_all_tabs(action, window, cx); + })) .when(PreviewTabsSettings::get_global(cx).enabled, |this| { this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| { if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { @@ -6452,57 +6402,6 @@ mod tests { .unwrap(); } - #[gpui::test] - async fn test_item_swapping_actions(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - assert_item_labels(&pane, [], cx); - - // Test that these actions do not panic - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs deleted file mode 100644 index cf463e6b22..0000000000 --- a/crates/workspace/src/path_list.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use util::paths::SanitizedPath; - -/// A list of absolute paths, in a specific order. -/// -/// The paths are stored in lexicographic order, so that they can be compared to -/// other path lists without regard to the order of the paths. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct PathList { - paths: Arc<[PathBuf]>, - order: Arc<[usize]>, -} - -#[derive(Debug)] -pub struct SerializedPathList { - pub paths: String, - pub order: String, -} - -impl PathList { - pub fn new>(paths: &[P]) -> Self { - let mut indexed_paths: Vec<(usize, PathBuf)> = paths - .iter() - .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) - .collect(); - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); - let paths = indexed_paths - .into_iter() - .map(|e| e.1) - .collect::>() - .into(); - Self { order, paths } - } - - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } - - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } - - pub fn is_lexicographically_ordered(&self) -> bool { - self.order.iter().enumerate().all(|(i, &j)| i == j) - } - - pub fn deserialize(serialized: &SerializedPathList) -> Self { - let mut paths: Vec = if serialized.paths.is_empty() { - Vec::new() - } else { - serialized.paths.split('\n').map(PathBuf::from).collect() - }; - - let mut order: Vec = serialized - .order - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); - - if !paths.is_sorted() || order.len() != paths.len() { - order = (0..paths.len()).collect(); - paths.sort(); - } - - Self { - paths: paths.into(), - order: order.into(), - } - } - - pub fn serialize(&self) -> SerializedPathList { - use std::fmt::Write as _; - - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } - - let mut order = String::new(); - for ix in self.order.iter() { - if !order.is_empty() { - order.push(','); - } - write!(&mut order, "{}", *ix).unwrap(); - } - SerializedPathList { paths, order } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_path_list() { - let list1 = PathList::new(&["a/d", "a/c"]); - let list2 = PathList::new(&["a/c", "a/d"]); - - assert_eq!(list1.paths(), list2.paths()); - assert_ne!(list1, list2); - assert_eq!(list1.order(), &[1, 0]); - assert_eq!(list2.order(), &[0, 1]); - - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1); - - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec..b2d1340a7b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,17 +9,15 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use collections::HashMap; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; +use client::DevServerProjectId; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; +use itertools::Itertools; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; +use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -30,17 +28,14 @@ use ui::{App, px}; use util::{ResultExt, maybe}; use uuid::Uuid; -use crate::{ - WorkspaceId, - path_list::{PathList, SerializedPathList}, -}; +use crate::WorkspaceId; use model::{ - GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, SshConnectionId, + GroupId, ItemId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedSshProject, SerializedWorkspace, }; -use self::model::{DockStructure, SerializedWorkspaceLocation}; +use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -279,189 +274,247 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -pub struct WorkspaceDb(ThreadSafeConnection); +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // local_paths: Bincode>, + // local_paths_order: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // fullscreen: Option, // Is the window fullscreen? + // centered_layout: Option, // Is the Centered Layout mode activated? + // session_id: Option, // Session id + // window_id: Option, // Window Id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Option, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // preview: bool // Indicates if this item is a preview item + // ) + // + // CREATE TABLE breakpoints( + // workspace_id: usize Foreign Key, // References workspace table + // path: PathBuf, // The absolute path of the file that this breakpoint belongs to + // breakpoint_location: Vec, // A list of the locations of breakpoints + // kind: int, // The kind of breakpoint (standard, log) + // log_message: String, // log message for log breakpoints, otherwise it's Null + // ) + pub static ref DB: WorkspaceDb<()> = + &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -473,172 +526,39 @@ impl Domain for WorkspaceDb { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); - - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths - ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - replace(workspaces.local_paths_array, ',', CHAR(10)) - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DELETE FROM workspaces_2 - WHERE workspace_id NOT IN ( - SELECT MAX(workspace_id) - FROM workspaces_2 - GROUP BY ssh_connection_id, paths - ); - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), - // Fix any data from when workspaces.paths were briefly encoded as JSON arrays - sql!( - UPDATE workspaces - SET paths = CASE - WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN - replace( - substr(paths, 3, length(paths) - 4), - '"' || ',' || '"', - CHAR(10) - ) - ELSE - replace(paths, ',', CHAR(10)) - END - WHERE paths IS NOT NULL - ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ) ]; - - // Allow recovering from bad migration that was initially shipped to nightly - // when introducing the ssh_connections table. - fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { - old.starts_with("CREATE TABLE ssh_connections") - && new.starts_with("CREATE TABLE ssh_connections") - } } -db::static_connection!(DB, WorkspaceDb, []); - impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -646,33 +566,17 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots>( &self, worktree_roots: &[P], - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, None) - } - - pub(crate) fn ssh_workspace_for_roots>( - &self, - worktree_roots: &[P], - ssh_project_id: SshConnectionId, - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) - } - - pub(crate) fn workspace_for_roots_internal>( - &self, - worktree_roots: &[P], - ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces - let root_paths = PathList::new(worktree_roots); + let local_paths = LocalPaths::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_bounds, display, centered_layout, @@ -680,8 +584,8 @@ impl WorkspaceDb { window_id, ): ( WorkspaceId, - String, - String, + Option, + Option, Option, Option, Option, @@ -691,8 +595,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_state, window_x, window_y, @@ -711,31 +615,25 @@ impl WorkspaceDb { bottom_dock_zoom, window_id FROM workspaces - WHERE - paths IS ? AND - ssh_connection_id IS ? - LIMIT 1 - }) - .map(|mut prepared_statement| { - (prepared_statement)(( - root_paths.serialize().paths, - ssh_connection_id.map(|id| id.0 as i32), - )) - .unwrap() + WHERE local_paths = ? }) + .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; - let paths = PathList::deserialize(&SerializedPathList { - paths, - order: paths_order, - }); + let local_paths = local_paths?; + let location = match local_paths_order { + Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), + None => { + let order = LocalPathsOrder::default_for_paths(&local_paths); + SerializedWorkspaceLocation::Local(local_paths, order) + } + }; Some(SerializedWorkspace { id: workspace_id, - location: SerializedWorkspaceLocation::Local, - paths, + location, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -750,6 +648,63 @@ impl WorkspaceDb { }) } + pub(crate) fn workspace_for_ssh_project( + &self, + ssh_project: &SerializedSshProject, + ) -> Option { + let (workspace_id, window_bounds, display, centered_layout, docks, window_id): ( + WorkspaceId, + Option, + Option, + Option, + DockStructure, + Option, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + centered_layout, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + window_id + FROM workspaces + WHERE ssh_project_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + window_bounds, + centered_layout: centered_layout.unwrap_or(false), + breakpoints: self.breakpoints(workspace_id), + display, + docks, + session_id: None, + window_id, + }) + } + fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -799,34 +754,16 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { - let paths = workspace.paths.serialize(); log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - let ssh_connection_id = match &workspace.location { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(connection) => { - Some(Self::get_or_create_ssh_connection_query( - conn, - connection.host.clone(), - connection.port, - connection.user.clone(), - )?.0) - } - }; - // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound( - sql!( - DELETE FROM breakpoints WHERE workspace_id = ?1; - DELETE FROM toolchains WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old breakpoints")?; + conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; for (path, breakpoints) in workspace.breakpoints { for bp in breakpoints { @@ -853,73 +790,115 @@ impl WorkspaceDb { } } } + } - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - ssh_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - ssh_connection_id, - )) - .context("clearing out old locations")?; - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - paths, - paths_order, - ssh_connection_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - paths = ?2, - paths_order = ?3, - ssh_connection_id = ?4, - left_dock_visible = ?5, - left_dock_active_panel = ?6, - left_dock_zoom = ?7, - right_dock_visible = ?8, - right_dock_active_panel = ?9, - right_dock_zoom = ?10, - bottom_dock_visible = ?11, - bottom_dock_active_panel = ?12, - bottom_dock_zoom = ?13, - session_id = ?14, - window_id = ?15, - timestamp = CURRENT_TIMESTAMP - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = ( - workspace.id, - paths.paths.clone(), - paths.order.clone(), - ssh_connection_id, - workspace.docks, - workspace.session_id, - workspace.window_id, - ); + match workspace.location { + SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? + ))?((&local_paths, workspace.id)) + .context("clearing out old locations")?; - prepared_query(args).context("Updating workspace")?; + // Upsert + let query = sql!( + INSERT INTO workspaces( + workspace_id, + local_paths, + local_paths_order, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp, + local_paths_array, + local_paths_order_array + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP, ?15, ?16) + ON CONFLICT DO + UPDATE SET + local_paths = ?2, + local_paths_order = ?3, + left_dock_visible = ?4, + left_dock_active_panel = ?5, + left_dock_zoom = ?6, + right_dock_visible = ?7, + right_dock_active_panel = ?8, + right_dock_zoom = ?9, + bottom_dock_visible = ?10, + bottom_dock_active_panel = ?11, + bottom_dock_zoom = ?12, + session_id = ?13, + window_id = ?14, + timestamp = CURRENT_TIMESTAMP, + local_paths_array = ?15, + local_paths_order_array = ?16 + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id, local_paths.paths().iter().map(|path| path.to_string_lossy().to_string()).join(","), local_paths_order.order().iter().map(|order| order.to_string()).join(",")); + + prepared_query(args).context("Updating workspace")?; + } + SerializedWorkspaceLocation::Ssh(ssh_project) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? + ))?((ssh_project.id.0, workspace.id)) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + ssh_project_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + ssh_project_id = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + session_id = ?12, + window_id = ?13, + timestamp = CURRENT_TIMESTAMP + ))?(( + workspace.id, + ssh_project.id.0, + workspace.docks, + workspace.session_id, + workspace.window_id + )) + .context("Updating workspace")?; + } + } // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -932,95 +911,89 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_connection( + pub(crate) async fn get_or_create_ssh_project( &self, host: String, port: Option, + paths: Vec, user: Option, - ) -> Result { - self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) - .await - } - - fn get_or_create_ssh_connection_query( - this: &Connection, - host: String, - port: Option, - user: Option, - ) -> Result { - if let Some(id) = this.select_row_bound(sql!( - SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 - ))?((host.clone(), port, user.clone()))? + ) -> Result { + let paths = serde_json::to_string(&paths)?; + if let Some(project) = self + .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) + .await? { - Ok(SshConnectionId(id)) + Ok(project) } else { log::debug!("Inserting SSH project at host {host}"); - let id = this.select_row_bound(sql!( - INSERT INTO ssh_connections ( - host, - port, - user - ) VALUES (?1, ?2, ?3) - RETURNING id - ))?((host, port, user))? - .context("failed to insert ssh project")?; - Ok(SshConnectionId(id)) + self.insert_ssh_project(host, port, paths, user) + .await? + .context("failed to insert ssh project") } } + query! { + async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects + WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? + LIMIT 1 + } + } + + query! { + async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + INSERT INTO ssh_projects( + host, + port, + paths, + user + ) VALUES (?1, ?2, ?3, ?4) + RETURNING id, host, port, paths, user + } + } + + query! { + pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result> { + UPDATE ssh_projects + SET paths = ?2 + WHERE id = ?1 + RETURNING id, host, port, paths, user + } + } + + pub(crate) async fn update_ssh_project_paths( + &self, + ssh_project_id: SshProjectId, + new_paths: Vec, + ) -> Result { + let paths = serde_json::to_string(&new_paths)?; + self.update_ssh_project_paths_query(ssh_project_id.0, paths) + .await? + .context("failed to update ssh project paths") + } + query! { pub async fn next_id() -> Result { INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } } - fn recent_workspaces(&self) -> Result)>> { - Ok(self - .recent_workspaces_query()? - .into_iter() - .map(|(id, paths, order, ssh_connection_id)| { - ( - id, - PathList::deserialize(&SerializedPathList { paths, order }), - ssh_connection_id, - ) - }) - .collect()) - } - query! { - fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, ssh_connection_id + fn recent_workspaces() -> Result)>> { + SELECT workspace_id, local_paths, local_paths_order, ssh_project_id FROM workspaces - WHERE - paths IS NOT NULL OR - ssh_connection_id IS NOT NULL + WHERE local_paths IS NOT NULL + OR ssh_project_id IS NOT NULL ORDER BY timestamp DESC } } - fn session_workspaces( - &self, - session_id: String, - ) -> Result, Option)>> { - Ok(self - .session_workspaces_query(session_id)? - .into_iter() - .map(|(paths, order, window_id, ssh_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - ssh_connection_id.map(SshConnectionId), - ) - }) - .collect()) - } - query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, ssh_connection_id + fn session_workspaces(session_id: String) -> Result, Option)>> { + SELECT local_paths, local_paths_order, window_id, ssh_project_id FROM workspaces - WHERE session_id = ?1 + WHERE session_id = ?1 AND dev_server_project_id IS NULL ORDER BY timestamp DESC } } @@ -1040,39 +1013,17 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { - Ok(self - .ssh_connections_query()? - .into_iter() - .map(|(id, host, port, user)| { - ( - SshConnectionId(id), - SerializedSshConnection { host, port, user }, - ) - }) - .collect()) - } - query! { - pub fn ssh_connections_query() -> Result, Option)>> { - SELECT id, host, port, user - FROM ssh_connections + fn ssh_projects() -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects } } - pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { - let row = self.ssh_connection_query(id.0)?; - Ok(SerializedSshConnection { - host: row.0, - port: row.1, - user: row.2, - }) - } - query! { - fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { - SELECT host, port, user - FROM ssh_connections + fn ssh_project(id: u64) -> Result { + SELECT id, host, port, paths, user + FROM ssh_projects WHERE id = ? } } @@ -1086,7 +1037,7 @@ impl WorkspaceDb { display, window_state, window_x, window_y, window_width, window_height FROM workspaces - WHERE paths + WHERE local_paths IS NOT NULL ORDER BY timestamp DESC LIMIT 1 @@ -1103,33 +1054,46 @@ impl WorkspaceDb { } } + pub async fn delete_workspace_by_dev_server_project_id( + &self, + id: DevServerProjectId, + ) -> Result<()> { + self.write(move |conn| { + conn.exec_bound(sql!( + DELETE FROM dev_server_projects WHERE id = ? + ))?(id.0)?; + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces + WHERE dev_server_project_id IS ? + ))?(id.0) + }) + .await + } + // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, - ) -> Result> { + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_connections = self.ssh_connections()?; + let ssh_projects = self.ssh_projects()?; - for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { - if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { - result.push(( - id, - SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), - paths, - )); + for (id, location, order, ssh_project_id) in self.recent_workspaces()? { + if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) { + if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) { + result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone()))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } continue; } - if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); + result.push((id, SerializedWorkspaceLocation::Local(location, order))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1139,13 +1103,13 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() .next() - .map(|(_, location, paths)| (location, paths))) + .map(|(_, location)| location)) } // Returns the locations of the workspaces that were still opened when the last @@ -1156,31 +1120,25 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, ssh_connection_id) in + for (location, order, window_id, ssh_project_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_connection_id) = ssh_connection_id { - workspaces.push(( - SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), - paths, - window_id.map(WindowId::from), - )); - } else if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if let Some(ssh_project_id) = ssh_project_id { + let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?); + workspaces.push((location, window_id.map(WindowId::from))); + } else if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - workspaces.push(( - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); + let location = SerializedWorkspaceLocation::Local(location, order); + workspaces.push((location, window_id.map(WindowId::from))); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { + workspaces.sort_by_key(|(_, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1189,7 +1147,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(location, paths, _)| (location, paths)) + .map(|(paths, _)| paths) .collect::>()) } @@ -1541,13 +1499,13 @@ pub fn delete_unloaded_items( #[cfg(test)] mod tests { + use std::thread; + use std::time::Duration; + use super::*; - use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }; + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; use gpui; - use pretty_assertions::assert_eq; - use std::{thread, time::Duration}; #[gpui::test] async fn test_breakpoints() { @@ -1600,8 +1558,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1754,8 +1711,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1801,8 +1757,7 @@ mod tests { let workspace_without_breakpoint = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1841,7 +1796,6 @@ mod tests { ON DELETE CASCADE ) STRICT; )], - |_, _, _| false, ) .unwrap(); }) @@ -1890,7 +1844,6 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], - |_, _, _| false, ) }) .await @@ -1898,8 +1851,7 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1912,8 +1864,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1942,7 +1893,7 @@ mod tests { }) .await; - workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); + workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -2018,8 +1969,10 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group, window_bounds: Default::default(), breakpoints: Default::default(), @@ -2051,8 +2004,10 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([0, 1]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2065,8 +2020,7 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2095,7 +2049,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); + workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -2106,8 +2060,10 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp2", "/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2125,7 +2081,8 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); + workspace_3.location = + SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -2143,8 +2100,7 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp1"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2157,8 +2113,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2171,8 +2126,7 @@ mod tests { let workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp3"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2185,8 +2139,7 @@ mod tests { let workspace_4 = SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(&["/tmp4"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2197,15 +2150,14 @@ mod tests { window_id: None, }; - let connection_id = db - .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) + let ssh_project = db + .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2218,8 +2170,10 @@ mod tests { let workspace_6 = SerializedWorkspace { id: WorkspaceId(6), - paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPathsOrder::new([2, 1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2241,36 +2195,41 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); - assert_eq!(locations[0].1, Some(20)); - assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); - assert_eq!(locations[1].1, Some(10)); + assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"])); + assert_eq!(locations[0].1, LocalPathsOrder::new([0])); + assert_eq!(locations[0].2, Some(20)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::default()); - assert_eq!(locations[0].1, Some(50)); - assert_eq!(locations[0].2, Some(connection_id)); - assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); - assert_eq!(locations[1].1, Some(30)); + let empty_paths: Vec<&str> = Vec::new(); + assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter())); + assert_eq!(locations[0].1, LocalPathsOrder::new([])); + assert_eq!(locations[0].2, Some(50)); + assert_eq!(locations[0].3, Some(ssh_project.id.0)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); assert_eq!( locations[0].0, - PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), ); - assert_eq!(locations[0].1, Some(60)); + assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0])); + assert_eq!(locations[0].2, Some(60)); } fn default_workspace>( - paths: &[P], + workspace_id: &[P], center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(paths), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(workspace_id), center_group: center_group.clone(), window_bounds: Default::default(), display: Default::default(), @@ -2293,18 +2252,30 @@ mod tests { WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; let workspaces = [ - (1, vec![dir1.path()], 9), - (2, vec![dir2.path()], 5), - (3, vec![dir3.path()], 8), - (4, vec![dir4.path()], 2), - (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), - (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), + (1, vec![dir1.path()], vec![0], 9), + (2, vec![dir2.path()], vec![0], 5), + (3, vec![dir3.path()], vec![0], 8), + (4, vec![dir4.path()], vec![0], 2), + ( + 5, + vec![dir1.path(), dir2.path(), dir3.path()], + vec![0, 1, 2], + 3, + ), + ( + 6, + vec![dir2.path(), dir3.path(), dir4.path()], + vec![2, 1, 0], + 4, + ), ] .into_iter() - .map(|(id, paths, window_id)| SerializedWorkspace { + .map(|(id, locations, order, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::new(paths.as_slice()), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(locations), + LocalPathsOrder::new(order), + ), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2329,37 +2300,39 @@ mod tests { WindowId::from(4), // Bottom ])); - let locations = db + let have = db .last_session_workspace_locations("one-session", stack) .unwrap(); + assert_eq!(have.len(), 6); assert_eq!( - locations, - [ - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir2.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) - ), - ] + have[0], + SerializedWorkspaceLocation::from_local_paths(&[dir4.path()]) + ); + assert_eq!( + have[1], + SerializedWorkspaceLocation::from_local_paths([dir3.path()]) + ); + assert_eq!( + have[2], + SerializedWorkspaceLocation::from_local_paths([dir2.path()]) + ); + assert_eq!( + have[3], + SerializedWorkspaceLocation::from_local_paths([dir1.path()]) + ); + assert_eq!( + have[4], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]), + LocalPathsOrder::new([0, 1, 2]), + ), + ); + assert_eq!( + have[5], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]), + LocalPathsOrder::new([2, 1, 0]), + ), ); } @@ -2370,7 +2343,7 @@ mod tests { ) .await; - let ssh_connections = [ + let ssh_projects = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2378,30 +2351,24 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string())) .await - .unwrap(); - SerializedSshConnection { - host: host.into(), - port: None, - user: Some(user.into()), - } + .unwrap() }) .collect::>(); - let ssh_connections = futures::future::join_all(ssh_connections).await; + let ssh_projects = futures::future::join_all(ssh_projects).await; let workspaces = [ - (1, ssh_connections[0].clone(), 9), - (2, ssh_connections[1].clone(), 5), - (3, ssh_connections[2].clone(), 8), - (4, ssh_connections[3].clone(), 2), + (1, ssh_projects[0].clone(), 9), + (2, ssh_projects[1].clone(), 5), + (3, ssh_projects[2].clone(), 8), + (4, ssh_projects[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_connection, window_id)| SerializedWorkspace { + .map(|(id, ssh_project, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(ssh_connection), + location: SerializedWorkspaceLocation::Ssh(ssh_project), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2430,31 +2397,19 @@ mod tests { assert_eq!(have.len(), 4); assert_eq!( have[0], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone()) ); assert_eq!( have[1], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone()) ); assert_eq!( have[2], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone()) ); assert_eq!( have[3], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone()) ); } @@ -2462,110 +2417,116 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - let host = "example.com".to_string(); - let port = Some(22_u16); - let user = Some("user".to_string()); + let (host, port, paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, user); + // Test that calling the function again with the same parameters returns the same project - let same_connection = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection); + assert_eq!(project.id, same_project.id); // Test with different parameters - let host2 = "otherexample.com".to_string(); - let port2 = None; - let user2 = Some("otheruser".to_string()); + let (host2, paths2, user2) = ( + "otherexample.com".to_string(), + vec!["/home/otheruser".to_string()], + Some("otheruser".to_string()), + ); - let different_connection = db - .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) + let different_project = db + .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) .await .unwrap(); - assert_ne!(connection_id, different_connection); + assert_ne!(project.id, different_project.id); + assert_eq!(different_project.host, host2); + assert_eq!(different_project.paths, paths2); + assert_eq!(different_project.user, user2); } #[gpui::test] async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - let (host, port, user) = ("example.com".to_string(), None, None); + let (host, port, paths, user) = ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, None) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) .await .unwrap(); - let same_connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, None); + + // Test that calling the function again with the same parameters returns the same project + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection_id); + assert_eq!(project.id, same_project.id); } #[gpui::test] - async fn test_get_ssh_connections() { - let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; + async fn test_get_ssh_projects() { + let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await; - let connections = [ - ("example.com".to_string(), None, None), + let projects = vec![ + ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ), ( "anotherexample.com".to_string(), Some(123_u16), + vec!["/home/user2".to_string()], Some("user2".to_string()), ), - ("yetanother.com".to_string(), Some(345_u16), None), + ( + "yetanother.com".to_string(), + Some(345_u16), + vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], + None, + ), ]; - let mut ids = Vec::new(); - for (host, port, user) in connections.iter() { - ids.push( - db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) - .await - .unwrap(), - ); + for (host, port, paths, user) in projects.iter() { + let project = db + .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(&project.host, host); + assert_eq!(&project.port, port); + assert_eq!(&project.paths, paths); + assert_eq!(&project.user, user); } - let stored_projects = db.ssh_connections().unwrap(); - assert_eq!( - stored_projects, - [ - ( - ids[0], - SerializedSshConnection { - host: "example.com".into(), - port: None, - user: None, - } - ), - ( - ids[1], - SerializedSshConnection { - host: "anotherexample.com".into(), - port: Some(123), - user: Some("user2".into()), - } - ), - ( - ids[2], - SerializedSshConnection { - host: "yetanother.com".into(), - port: Some(345), - user: None, - } - ), - ] - .into_iter() - .collect::>(), - ); + let stored_projects = db.ssh_projects().unwrap(); + assert_eq!(stored_projects.len(), projects.len()); } #[gpui::test] @@ -2698,4 +2659,56 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } + + #[gpui::test] + async fn test_update_ssh_project_paths() { + zlog::init_test(); + + let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await; + + let (host, port, initial_paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); + + let project = db + .get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(project.host, host); + assert_eq!(project.paths, initial_paths); + assert_eq!(project.user, user); + + let new_paths = vec![ + "/home/user".to_string(), + "/etc/nginx".to_string(), + "/var/log".to_string(), + "/opt/app".to_string(), + ]; + + let updated_project = db + .update_ssh_project_paths(project.id, new_paths.clone()) + .await + .unwrap(); + + assert_eq!(updated_project.id, project.id); + assert_eq!(updated_project.paths, new_paths); + + let retrieved_project = db + .get_ssh_project( + host.clone(), + port, + serde_json::to_string(&new_paths).unwrap(), + user.clone(), + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(retrieved_project.id, project.id); + assert_eq!(retrieved_project.paths, new_paths); + } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 04757d0495..15a54ac62f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,48 +1,256 @@ use super::{SerializedAxis, SerializedWindowBounds}; use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, - path_list::PathList, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use async_recursion::async_recursion; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; - +use itertools::Itertools as _; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; +use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, }; -use util::ResultExt; +use util::{ResultExt, paths::SanitizedPath}; use uuid::Uuid; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub(crate) struct SshConnectionId(pub u64); - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshConnection { +pub struct SerializedSshProject { + pub id: SshProjectId, pub host: String, pub port: Option, + pub paths: Vec, pub user: Option, } +impl SerializedSshProject { + pub fn ssh_urls(&self) -> Vec { + self.paths + .iter() + .map(|path| { + let mut result = String::new(); + if let Some(user) = &self.user { + result.push_str(user); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = &self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result.push_str(path); + PathBuf::from(result) + }) + .collect() + } +} + +impl StaticColumnCount for SerializedSshProject { + fn column_count() -> usize { + 5 + } +} + +impl Bind for &SerializedSshProject { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.id.0, start_index)?; + let next_index = statement.bind(&self.host, next_index)?; + let next_index = statement.bind(&self.port, next_index)?; + let raw_paths = serde_json::to_string(&self.paths)?; + let next_index = statement.bind(&raw_paths, next_index)?; + statement.bind(&self.user, next_index) + } +} + +impl Column for SerializedSshProject { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let id = statement.column_int64(start_index)?; + let host = statement.column_text(start_index + 1)?.to_string(); + let (port, _) = Option::::column(statement, start_index + 2)?; + let raw_paths = statement.column_text(start_index + 3)?.to_string(); + let paths: Vec = serde_json::from_str(&raw_paths)?; + + let (user, _) = Option::::column(statement, start_index + 4)?; + + Ok(( + Self { + id: SshProjectId(id as u64), + host, + port, + paths, + user, + }, + start_index + 5, + )) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPaths(Arc>); + +impl LocalPaths { + pub fn new>(paths: impl IntoIterator) -> Self { + let mut paths: Vec = paths + .into_iter() + .map(|p| SanitizedPath::from(p).into()) + .collect(); + // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace. + // The actual workspace order is stored in the `LocalPathsOrder` struct. + paths.sort(); + Self(Arc::new(paths)) + } + + pub fn paths(&self) -> &Arc> { + &self.0 + } +} + +impl StaticColumnCount for LocalPaths {} +impl Bind for &LocalPaths { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPaths { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let path_blob = statement.column_blob(start_index)?; + let paths: Arc> = if path_blob.is_empty() { + Default::default() + } else { + bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? + }; + + Ok((Self(paths), start_index + 1)) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPathsOrder(Vec); + +impl LocalPathsOrder { + pub fn new(order: impl IntoIterator) -> Self { + Self(order.into_iter().collect()) + } + + pub fn order(&self) -> &[usize] { + self.0.as_slice() + } + + pub fn default_for_paths(paths: &LocalPaths) -> Self { + Self::new(0..paths.0.len()) + } +} + +impl StaticColumnCount for LocalPathsOrder {} +impl Bind for &LocalPathsOrder { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPathsOrder { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let order_blob = statement.column_blob(start_index)?; + let order = if order_blob.is_empty() { + Vec::new() + } else { + bincode::deserialize(order_blob).context("deserializing workspace root order")? + }; + + Ok((Self(order), start_index + 1)) + } +} + #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { - Local, - Ssh(SerializedSshConnection), + Local(LocalPaths, LocalPathsOrder), + Ssh(SerializedSshProject), } impl SerializedWorkspaceLocation { + /// Create a new `SerializedWorkspaceLocation` from a list of local paths. + /// + /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]); + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![0, 1]), + /// )); + /// ``` + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace2"), + /// Path::new("path/to/workspace1"), + /// ]); + /// + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![1, 0]), + /// )); + /// ``` + pub fn from_local_paths>(paths: impl IntoIterator) -> Self { + let mut indexed_paths: Vec<_> = paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .enumerate() + .collect(); + + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + + let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect(); + let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect(); + + Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order)) + } + /// Get sorted paths pub fn sorted_paths(&self) -> Arc> { - unimplemented!() + match self { + SerializedWorkspaceLocation::Local(paths, order) => { + if order.order().is_empty() { + paths.paths().clone() + } else { + Arc::new( + order + .order() + .iter() + .zip(paths.paths().iter()) + .sorted_by_key(|(i, _)| **i) + .map(|(_, p)| p.clone()) + .collect(), + ) + } + } + SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), + } } } @@ -50,7 +258,6 @@ impl SerializedWorkspaceLocation { pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, - pub(crate) paths: PathList, pub(crate) center_group: SerializedPaneGroup, pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, @@ -374,3 +581,80 @@ impl Column for SerializedItem { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize_local_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + + assert_eq!( + serialized, + SerializedWorkspaceLocation::Local( + LocalPaths::new(vec!["a", "b", "c"]), + LocalPathsOrder::new(vec![1, 0, 2]) + ) + ); + } + + #[test] + fn test_sorted_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("a"), + PathBuf::from("c"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![2, 0, 1]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("c"), + PathBuf::from("a"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); + assert_eq!(serialized.sorted_paths(), paths); + + let urls = ["/a", "/b", "/c"]; + let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject { + id: SshProjectId(0), + host: "host".to_string(), + port: Some(22), + paths: urls.iter().map(|s| s.to_string()).collect(), + user: Some("user".to_string()), + }); + assert_eq!( + serialized.sorted_paths(), + Arc::new( + urls.iter() + .map(|p| PathBuf::from(format!("user@host:22{}", p))) + .collect() + ) + ); + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..499e4f4619 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,12 +1,10 @@ pub mod dock; pub mod history_manager; -pub mod invalid_buffer_view; pub mod item; mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; -mod path_list; mod persistence; pub mod searchable; pub mod shared_screen; @@ -19,7 +17,6 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -64,10 +61,13 @@ use notifications::{ }; pub use pane::*; pub use pane_group::*; -use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; +use persistence::{ + DB, SerializedWindowBounds, + model::{SerializedSshProject, SerializedWorkspace}, +}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, + model::{ItemId, LocalPaths, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ @@ -612,60 +612,21 @@ impl ProjectItemRegistry { ); self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { - let project_path = project_path.clone(); - let is_file = project - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| entry.is_file()); - let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); - let is_local = project.read(cx).is_local(); let project_item = - ::try_open(project, &project_path, cx)?; + ::try_open(project, project_path, cx)?; let project = project.clone(); Some(window.spawn(cx, async move |cx| { - match project_item.await.with_context(|| { - format!( - "opening project path {:?}", - entry_abs_path.as_deref().unwrap_or(&project_path.path) - ) - }) { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - Ok((None, build_workspace_item)) - } - None => Err(e)?, - }, - None => Err(e)?, + let project_item = project_item.await?; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item(project, Some(pane), project_item, window, cx) + })) as Box }, - } + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) })) }); } @@ -1052,7 +1013,7 @@ pub enum OpenVisible { enum WorkspaceLocation { // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation, PathList), + Location(SerializedWorkspaceLocation), // No valid location found hence clear session id DetachFromSession, // No valid location found to serialize @@ -1136,6 +1097,7 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, + serialized_ssh_project: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1184,6 +1146,8 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); + this.update_ssh_paths(cx); + this.serialize_ssh_paths(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. this.update_history(cx); @@ -1468,7 +1432,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - + serialized_ssh_project: None, scheduled_tasks: Vec::new(), } } @@ -1508,9 +1472,20 @@ impl Workspace { let serialized_workspace = persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { - paths_to_open = paths.paths().to_vec(); - if !paths.is_lexicographically_ordered() { + let workspace_location = serialized_workspace + .as_ref() + .map(|ws| &ws.location) + .and_then(|loc| match loc { + SerializedWorkspaceLocation::Local(_, order) => { + Some((loc.sorted_paths(), order.order())) + } + _ => None, + }); + + if let Some((paths, order)) = workspace_location { + paths_to_open = paths.iter().cloned().collect(); + + if order.iter().enumerate().any(|(i, &j)| i != j) { project_handle .update(cx, |project, cx| { project.set_worktrees_reordered(true, cx); @@ -2030,6 +2005,14 @@ impl Workspace { self.debugger_provider.clone() } + pub fn serialized_ssh_project(&self) -> Option { + self.serialized_ssh_project.clone() + } + + pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) { + self.serialized_ssh_project = Some(serialized_ssh_project); + } + pub fn prompt_for_open_path( &mut self, path_prompt_options: PathPromptOptions, @@ -2266,43 +2249,27 @@ impl Workspace { })?; if let Some(active_call) = active_call + && close_intent != CloseIntent::Quit && workspace_count == 1 && active_call.read_with(cx, |call, _| call.room().is_some())? { - if close_intent == CloseIntent::CloseWindow { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } - } - if close_intent == CloseIntent::ReplaceWindow { - _ = active_call.update(cx, |this, cx| { - let workspace = cx - .windows() - .iter() - .filter_map(|window| window.downcast::()) - .next() - .unwrap(); - let project = workspace.read(cx)?.project.clone(); - if project.read(cx).is_shared() { - this.unshare_project(project, cx)?; - } - Ok::<_, anyhow::Error>(()) - })?; + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); } } @@ -3396,8 +3363,9 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { + let project = self.project().clone(); let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) + registry.open_path(&project, &path, window, cx) } pub fn find_project_item( @@ -4022,6 +3990,52 @@ impl Workspace { maybe_pane_handle } + pub fn split_pane_with_item( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + from: WeakEntity, + item_id_to_move: EntityId, + window: &mut Window, + cx: &mut Context, + ) { + let Some(pane_to_split) = pane_to_split.upgrade() else { + return; + }; + let Some(from) = from.upgrade() else { + return; + }; + + let new_pane = self.add_pane(window, cx); + move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + } + + pub fn split_pane_with_project_entry( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + project_entry: ProjectEntryId, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let pane_to_split = pane_to_split.upgrade()?; + let new_pane = self.add_pane(window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + + let path = self.project.read(cx).path_for_entry(project_entry, cx)?; + let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); + Some(cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + })) + } + pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { @@ -5030,12 +5044,59 @@ impl Workspace { self.session_id.clone() } - pub fn root_paths(&self, cx: &App) -> Vec> { + fn local_paths(&self, cx: &App) -> Option>> { let project = self.project().read(cx); - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() + + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>(), + ) + } else { + None + } + } + + fn update_ssh_paths(&mut self, cx: &App) { + let project = self.project().read(cx); + if !project.is_local() { + let paths: Vec = project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) + .collect(); + if let Some(ssh_project) = &mut self.serialized_ssh_project { + ssh_project.paths = paths; + } + } + } + + fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context) { + if self._schedule_serialize_ssh_paths.is_none() { + self._schedule_serialize_ssh_paths = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; + this.update_in(cx, |this, window, cx| { + let task = if let Some(ssh_project) = &this.serialized_ssh_project { + let ssh_project_id = ssh_project.id; + let ssh_project_paths = ssh_project.paths.clone(); + window.spawn(cx, async move |_| { + persistence::DB + .update_ssh_project_paths(ssh_project_id, ssh_project_paths) + .await + }) + } else { + Task::ready(Err(anyhow::anyhow!("No SSH project to serialize"))) + }; + task.detach(); + this._schedule_serialize_ssh_paths.take(); + }) + .log_err(); + })); + } } fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { @@ -5208,7 +5269,7 @@ impl Workspace { } match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location, paths) => { + WorkspaceLocation::Location(location) => { let breakpoints = self.project.update(cx, |project, cx| { project .breakpoint_store() @@ -5222,7 +5283,6 @@ impl Workspace { let serialized_workspace = SerializedWorkspace { id: database_id, location, - paths, center_group, window_bounds, display: Default::default(), @@ -5248,19 +5308,13 @@ impl Workspace { } fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { - WorkspaceLocation::Location( - SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - host: connection.host, - port: connection.port, - user: connection.username, - }), - paths, - ) - } else if self.project.read(cx).is_local() { - if !paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) + if let Some(ssh_project) = &self.serialized_ssh_project { + WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) + } else if let Some(local_paths) = self.local_paths(cx) { + if !local_paths.is_empty() { + WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( + local_paths, + )) } else { WorkspaceLocation::DetachFromSession } @@ -5273,13 +5327,13 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - if !self.project.read(cx).is_local() { - return; - } + let location = match self.serialize_workspace_location(cx) { + WorkspaceLocation::Location(location) => location, + _ => return, + }; if let Some(manager) = HistoryManager::global(cx) { - let paths = PathList::new(&self.root_paths(cx)); manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); + this.update_history(id, HistoryManagerEntry::new(id, &location), cx); }); } } @@ -6587,29 +6641,15 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() + Some(div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(zoomed_view) + .child(view.upgrade()?) .inset_0() - .shadow_lg(); - - if !WorkspaceSettings::get_global(cx).zoomed_padding { - return Some(div); - } - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) + .shadow_lg()) })) .children(self.render_notifications(window, cx)), ) @@ -6759,14 +6799,14 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { +pub async fn last_opened_workspace_location() -> Option { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -7269,7 +7309,7 @@ pub fn open_ssh_project_with_new_connection( cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx @@ -7303,6 +7343,7 @@ pub fn open_ssh_project_with_new_connection( open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7322,12 +7363,13 @@ pub fn open_ssh_project_with_existing_connection( cx: &mut AsyncApp, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7341,6 +7383,7 @@ pub fn open_ssh_project_with_existing_connection( async fn open_ssh_project_inner( project: Entity, paths: Vec, + serialized_ssh_project: SerializedSshProject, workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, @@ -7393,6 +7436,7 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); + workspace.set_serialized_ssh_project(serialized_ssh_project); workspace.update_history(cx); if let Some(ref serialized) = serialized_workspace { @@ -7429,18 +7473,28 @@ fn serialize_ssh_project( connection_options: SshConnectionOptions, paths: Vec, cx: &AsyncApp, -) -> Task)>> { +) -> Task< + Result<( + SerializedSshProject, + WorkspaceId, + Option, + )>, +> { cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection( + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project( connection_options.host.clone(), connection_options.port, + paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(), connection_options.username.clone(), ) .await?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -7450,7 +7504,7 @@ fn serialize_ssh_project( persistence::DB.next_id().await? }; - Ok((workspace_id, serialized_workspace)) + Ok((serialized_ssh_project, workspace_id, serialized_workspace)) }) } @@ -7997,15 +8051,18 @@ pub fn ssh_workspace_position_from_db( paths_to_open: &[PathBuf], cx: &App, ) -> Task> { - let paths = paths_to_open.to_vec(); + let paths = paths_to_open + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(); cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection(host, port, user) + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project(host, port, paths, user) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 3b6bc1ea97..5635347514 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,7 +29,6 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, - pub zoomed_padding: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -203,12 +202,6 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, - /// Whether to show padding for zoomed panels. - /// When enabled, zoomed bottom panels will have some top padding, - /// while zoomed left/right panels will have padding to the right/left (respectively). - /// - /// Default: true - pub zoomed_padding: Option, } #[derive(Deserialize)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb..ac4cd72124 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -20,7 +20,6 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true -acp_tools.workspace = true agent.workspace = true agent_ui.workspace = true agent_settings.workspace = true @@ -34,7 +33,6 @@ audio.workspace = true auto_update.workspace = true auto_update_ui.workspace = true backtrace = "0.3" -bincode.workspace = true breadcrumbs.workspace = true call.workspace = true channel.workspace = true @@ -62,7 +60,6 @@ extensions_ui.workspace = true feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true -system_specs.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist deleted file mode 100644 index fd2a4101d8..0000000000 --- a/crates/zed/resources/info/SupportedPlatforms.plist +++ /dev/null @@ -1,4 +0,0 @@ -CFBundleSupportedPlatforms - - MacOSX - diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e99c8b564b..7ab76b71de 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,7 +16,7 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; +use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -47,8 +47,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, + AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, + notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -240,7 +240,7 @@ pub fn main() { option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); if args.system_specs { - let system_specs = system_specs::SystemSpecs::new_stateless( + let system_specs = feedback::system_specs::SystemSpecs::new_stateless( app_version, app_commit_sha, *release_channel::RELEASE_CHANNEL, @@ -566,7 +566,6 @@ pub fn main() { language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); agent_settings::init(cx); agent_servers::init(cx); - acp_tools::init(cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); @@ -949,14 +948,15 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let mut tasks = Vec::new(); - for (location, paths) in locations { + for location in locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(location, _) => { let app_state = app_state.clone(); + let paths = location.paths().to_vec(); let task = cx.spawn(async move |cx| { let open_task = cx.update(|cx| { workspace::open_paths( - &paths.paths(), + &paths, app_state, workspace::OpenOptions::default(), cx, @@ -978,7 +978,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp match connection_options { Ok(connection_options) => recent_projects::open_ssh_project( connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, workspace::OpenOptions::default(), cx, @@ -1069,7 +1069,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ac06f1fd9f..f55468280c 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -60,9 +60,7 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); - if *release_channel::RELEASE_CHANNEL != ReleaseChannel::Dev { - crashes::handle_panic(payload.clone(), info.location()); - } + crashes::handle_panic(payload.clone(), info.location()); let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); @@ -89,9 +87,7 @@ pub fn init_panic_hook( }, backtrace, ); - if MINIDUMP_ENDPOINT.is_none() { - std::process::exit(-1); - } + std::process::exit(-1); } let main_module_base_address = get_main_module_base_address(); @@ -150,9 +146,7 @@ pub fn init_panic_hook( } zlog::flush(); - if (!is_pty || MINIDUMP_ENDPOINT.is_some()) - && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() - { + if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); let panic_file = fs::OpenOptions::new() @@ -257,7 +251,6 @@ pub fn init( endpoint, minidump_contents, &metadata, - installation_id.clone(), ) .await .log_err(); @@ -485,9 +478,7 @@ fn upload_panics_and_crashes( return; } cx.background_spawn(async move { - upload_previous_minidumps(http.clone(), installation_id.clone()) - .await - .warn_on_err(); + upload_previous_minidumps(http.clone()).await.warn_on_err(); let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() @@ -555,10 +546,7 @@ async fn upload_previous_panics( Ok(most_recent_panic) } -pub async fn upload_previous_minidumps( - http: Arc, - installation_id: Option, -) -> anyhow::Result<()> { +pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { log::warn!("Minidump endpoint not set"); return Ok(()); @@ -581,7 +569,6 @@ pub async fn upload_previous_minidumps( .await .context("Failed to read minidump")?, &metadata, - installation_id.clone(), ) .await .log_err() @@ -599,7 +586,6 @@ async fn upload_minidump( endpoint: &str, minidump: Vec, metadata: &crashes::CrashInfo, - installation_id: Option, ) -> Result<()> { let mut form = Form::new() .part( @@ -615,83 +601,15 @@ async fn upload_minidump( .text("sentry[tags][version]", metadata.init.zed_version.clone()) .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); - let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { - panic_message = panic_info.message.clone(); - form = form - .text("sentry[logentry][formatted]", panic_info.message.clone()) - .text("span", panic_info.span.clone()); + form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); + form = form.text("span", panic_info.span.clone()); + // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu + // name, screen resolution, available ram, device model, etc } if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); } - if let Some(id) = installation_id.clone() { - form = form.text("sentry[user][id]", id) - } - - ::telemetry::event!( - "Minidump Uploaded", - panic_message = panic_message, - crashed_version = metadata.init.zed_version.clone(), - commit_sha = metadata.init.commit_sha.clone(), - ); - - let gpu_count = metadata.gpus.len(); - for (index, gpu) in metadata.gpus.iter().cloned().enumerate() { - let system_specs::GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - } = gpu; - let num = if gpu_count == 1 && metadata.active_gpu.is_none() { - String::new() - } else { - index.to_string() - }; - let name = format!("gpu{num}"); - let root = format!("sentry[contexts][{name}]"); - form = form - .text( - format!("{root}[Description]"), - "A GPU found on the users system. May or may not be the GPU Zed is running on", - ) - .text(format!("{root}[type]"), "gpu") - .text(format!("{root}[name]"), device_name.unwrap_or(name)) - .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id)) - .text( - format!("{root}[vendor_id]"), - format!("{:#06x}", vendor_pci_id), - ) - .text_if_some(format!("{root}[vendor_name]"), vendor_name) - .text_if_some(format!("{root}[driver_version]"), driver_version) - .text_if_some(format!("{root}[driver_name]"), driver_name); - } - if let Some(active_gpu) = metadata.active_gpu.clone() { - form = form - .text( - "sentry[contexts][Active_GPU][Description]", - "The GPU Zed is running on", - ) - .text("sentry[contexts][Active_GPU][type]", "gpu") - .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name) - .text( - "sentry[contexts][Active_GPU][driver_version]", - active_gpu.driver_info, - ) - .text( - "sentry[contexts][Active_GPU][driver_name]", - active_gpu.driver_name, - ) - .text( - "sentry[contexts][Active_GPU][is_software_emulated]", - active_gpu.is_software_emulated.to_string(), - ); - } - - // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; @@ -706,27 +624,6 @@ async fn upload_minidump( Ok(()) } -trait FormExt { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self; -} - -impl FormExt for Form { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self { - match value { - Some(value) => self.text(label.into(), value.into()), - None => self, - } - } -} - async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdb..3b5f99f9bd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -344,17 +344,7 @@ pub fn initialize_workspace( if let Some(specs) = window.gpu_specs() { log::info!("Using GPU: {:?}", specs); - show_software_emulation_warning_if_needed(specs.clone(), window, cx); - if let Some((crash_server, message)) = crashes::CRASH_HANDLER - .get() - .zip(bincode::serialize(&specs).ok()) - && let Err(err) = crash_server.send_message(3, message) - { - log::warn!( - "Failed to store active gpu info for crash reporting: {}", - err - ); - } + show_software_emulation_warning_if_needed(specs, window, cx); } let edit_prediction_menu_handle = PopoverMenuHandle::default(); @@ -1308,11 +1298,11 @@ pub fn handle_keymap_file_changes( }) .detach(); - let mut current_layout_id = cx.keyboard_layout().id().to_string(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); cx.on_keyboard_layout_change(move |cx| { - let next_layout_id = cx.keyboard_layout().id(); - if next_layout_id != current_layout_id { - current_layout_id = next_layout_id.to_string(); + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); + if next_mapping != current_mapping { + current_mapping = next_mapping; keyboard_layout_tx.unbounded_send(()).ok(); } }) @@ -4729,7 +4719,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), "On {} Failed to find {} with key binding {}", line, action.name(), diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index c37a4cc389..780f7f7626 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,17 +1,10 @@ use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -pub struct ComponentPreviewDb(ThreadSafeConnection); - -impl Domain for ComponentPreviewDb { - const NAME: &str = stringify!(ComponentPreviewDb); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection! { + pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = + &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -20,11 +13,9 @@ impl Domain for ComponentPreviewDb { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } -db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); - impl ComponentPreviewDb { pub async fn save_active_page( &self, diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index bc2d757fd1..a9abd9bc74 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -75,10 +75,13 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { + let tos_accepted = user_store.read(cx).has_accepted_terms_of_service(); + telemetry::event!( "Edit Prediction Provider Changed", from = provider, to = new_provider, + zed_ai_tos_accepted = tos_accepted, ); provider = new_provider; @@ -89,6 +92,28 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { user_store.clone(), cx, ); + + if !tos_accepted { + match provider { + EditPredictionProvider::Zed => { + let Some(window) = cx.active_window() else { + return; + }; + + window + .update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenZedPredictOnboarding), + cx, + ); + }) + .ok(); + } + EditPredictionProvider::None + | EditPredictionProvider::Copilot + | EditPredictionProvider::Supermaven => {} + } + } } } }) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2194fb7af5..827c7754fa 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -26,7 +26,6 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; -use workspace::PathList; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -362,14 +361,12 @@ async fn open_workspaces( if open_new_workspace == Some(true) { Vec::new() } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() + let locations = restorable_workspace_locations(cx, &app_state).await; + locations.unwrap_or_default() } } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), + vec![SerializedWorkspaceLocation::from_local_paths( + paths.into_iter().map(PathBuf::from), )] }; @@ -397,9 +394,9 @@ async fn open_workspaces( // If there are paths to open, open a workspace for each grouping of paths let mut errored = false; - for (location, workspace_paths) in grouped_locations { + for location in grouped_locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(workspace_paths, _) => { let workspace_paths = workspace_paths .paths() .iter() @@ -432,7 +429,7 @@ async fn open_workspaces( cx.spawn(async move |cx| { open_ssh_project( connection_options, - workspace_paths.paths().to_vec(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions::default(), cx, diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index fb5a75f78d..3772104f39 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -72,10 +72,7 @@ impl QuickActionBar { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) - ), + format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), window, cx, ) diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca49..9455369e9a 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -156,10 +156,7 @@ pub mod workspace { #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] CopyPath, #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath, - /// Opens the selected file with the system's default application. - #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] - OpenWithSystem, + CopyRelativePath ] ); } @@ -284,17 +281,13 @@ pub mod agent { OpenSettings, /// Opens the agent onboarding modal. OpenOnboardingModal, - /// Opens the ACP onboarding modal. - OpenAcpOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. Chat, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector, - /// Triggers re-authentication on Gemini - ReauthenticateAgent + ToggleModelSelector ] ); } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 7b14d12796..916699d29b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -118,8 +118,12 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal() -> bool { - !ZedPredictUpsell::dismissed() +pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { + if user_store.read(cx).has_accepted_terms_of_service() { + !ZedPredictUpsell::dismissed() + } else { + true + } } #[derive(Clone)] @@ -1543,6 +1547,16 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { ) -> bool { true } + + fn needs_terms_acceptance(&self, cx: &App) -> bool { + !self + .zeta + .read(cx) + .user_store + .read(cx) + .has_accepted_terms_of_service() + } + fn is_refreshing(&self) -> bool { !self.pending_completions.is_empty() } @@ -1555,6 +1569,10 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { _debounce: bool, cx: &mut Context, ) { + if self.needs_terms_acceptance(cx) { + return; + } + if self.zeta.read(cx).update_required { return; } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 251cad6234..9d07881914 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -47,6 +47,7 @@ - [Overview](./ai/overview.md) - [Agent Panel](./ai/agent-panel.md) - [Tools](./ai/tools.md) + - [External Agents](./ai/external-agents.md) - [Inline Assistant](./ai/inline-assistant.md) - [Edit Prediction](./ai/edit-prediction.md) - [Text Threads](./ai/text-threads.md) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index f944eb88b0..9b39f9abdd 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -1,14 +1,15 @@ # Agent Panel -The Agent Panel provides you with a surface to interact with LLMs, enabling various types of tasks, such as generating code, asking questions about your codebase, and general inquiries like emails, documentation, and more. +The Agent Panel allows you to interact with many LLMs and coding agents that can support you in various types of tasks, such as generating code, codebase understanding, and other general inquiries like writing emails, documentation, and more. To open it, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. -If you're using the Agent Panel for the first time, you need to have at least one LLM provider configured. +If you're using the Agent Panel for the first time, you need to have at least one LLM or agent provider configured. You can do that by: 1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models -2. or by [bringing your own API keys](./llm-providers.md#use-your-own-keys) for your desired provider +2. [bringing your own API keys](./llm-providers.md#use-your-own-keys) for your desired provider +3. using an external agent like [Gemini CLI](./external-agents.md#gemini-cli) ## Overview {#overview} @@ -17,6 +18,17 @@ If you need extra room to type, you can expand the message editor with {#kb agen You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. +> Note that, currently, not all features outlined below work for external agents, like Gemini CLI. +> Features like _checkpoints_, _token usage display_, and _model selection_ may be supported in the future for them. + +### Creating New Threads + +The default view for the Agent Panel uses Zed's first-party agent. +Every time that you hit {#kb agent::NewThread}, it creates a new thread using the currently selected agent. + +To change that, go to the plus button in the top-right of the Agent Panel and choose another option. +You choose to create a new [Text Thread](./text-threads.md) or, if you have [external agents](/.external-agents.md) connected, you can create new threads with them. + ### Editing Messages {#editing-messages} Any message that you send to the AI is editable. @@ -30,7 +42,7 @@ The checkpoint button appears even if you interrupt the thread midway through an ### Navigating History {#navigating-history} -To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top left of the panel to open the dropdown that shows you the six most recent threads. +To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top right of the panel to open the dropdown that shows you the six most recent threads. The items in this menu function similarly to tabs, and closing them doesn’t delete the thread; instead, it simply removes them from the recent list. @@ -70,16 +82,13 @@ So, if your active tab had edits made by the AI, you'll see diffs with the same Although Zed's agent is very efficient at reading through your code base to autonomously pick up relevant files, directories, and other context, manually adding context is still encouraged as a way to speed up and improve the AI's response quality. -If you have a tab open while using the Agent Panel, that tab appears as a suggested context in form of a dashed button. -You can also add other forms of context by either mentioning them with `@` or hitting the `+` icon button. - -You can even add previous threads as context by mentioning them with `@thread`, or by selecting the "New From Summary" option from the `+` menu to continue a longer conversation, keeping it within the context window. +To add any file, directory, symbol, previous threads, rules files, or even web pages as context, type `@` to mention them in the editor. Pasting images as context is also supported by the Agent Panel. ### Token Usage {#token-usage} -Zed surfaces how many tokens you are consuming for your currently active thread in the panel's toolbar. +Zed surfaces how many tokens you are consuming for your currently active thread nearby the profile selector in the panel's message editor. Depending on how many pieces of context you add, your token consumption can grow rapidly. With that in mind, once you get close to the model's context window, a banner appears below the message editor suggesting to start a new thread with the current one summarized and added as context. @@ -145,7 +154,7 @@ Zed's UI will inform about this via a warning icon that appears close to the mod ## Text Threads {#text-threads} -["Text threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. +["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. With text threads, you have full control over the conversation data. You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation. diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md new file mode 100644 index 0000000000..c50b1cdc49 --- /dev/null +++ b/docs/src/ai/external-agents.md @@ -0,0 +1,81 @@ +# External Agents + +Zed supports terminal-based agentic coding tools through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). + +Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. + +## Gemini CLI {#gemini-cli} + +Zed provides the ability to run [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly in the [agent panel](./agent-panel.md). + +Under the hood we run Gemini CLI in the background, and talk to it over ACP. +This means that you're running the real Gemini CLI, with all of the advantages of that, but you can see and interact with files in your editor. + +### Getting Started + +As of Zed Stable v0.201.5 you should be able to use Gemini CLI directly from Zed. First open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a New Gemini CLI thread. + +If you'd like to bind this to a keyboard shortcut, you can do so by editing your keybindings file to include: + +```json +[ + { + "bindings": { + "cmd-alt-g": ["agent::NewExternalAgentThread", { "agent": "gemini" }] + } + } +] +``` + +#### Installation + +If you don't yet have Gemini CLI installed, then Zed will install a version for you. If you do, then we will use the version of Gemini CLI on your path. + +You need to be running at least Gemini version `0.2.0-preview`, and if your version of Gemini is too old you will see an +error message. + +The instructions to upgrade Gemini depend on how you originally installed it, but typically, running `npm install -g gemini-cli@preview` should work. + +#### Authentication + +After you have Gemini CLI running, you'll be prompted to choose your authentication method. + +Most users should click the "Log in with Google". This will cause a browser window to pop-up and auth directly with Gemini CLI. Zed does not see your oauth or access tokens in this case. + +You can also use the "Gemini API Key". If you select this, and have the `GEMINI_API_KEY` set, then we will use that. Otherwise Zed will prompt you for an API key which will be stored securely in your keychain, and used to start Gemini CLI from within Zed. + +The "Vertex AI" option is for those who are using Vertex AI, and have already configured their environment correctly. + +For more information, see the [Gemini CLI docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md). + +### Usage + +Similar to Zed's first-party agent, you can use Gemini CLI to do anything that you need. + +You can @-mention files, recent conversations, symbols, or fetch the web. + +There are two features that don't yet work with Gemini CLI: editing past messages, which we hope to add support for soon; and resuming a conversation from history. + +## Add Custom Agents {#add-custom-agents} + +You can run any agent speaking ACP in Zed by changing your settings as follows: + +```json +{ + "agent_servers": { + "Custom Agent": { + "command": "node", + "args": ["~/projects/agent/index.js", "--acp"], + "env": {} + } + } +} +``` + +This can also be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it. + +## Debugging Agents + +When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent. + +[screenshot here] diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md index 6f081cb243..8bd45240fd 100644 --- a/docs/src/ai/overview.md +++ b/docs/src/ai/overview.md @@ -6,6 +6,8 @@ Learn how to get started using AI with Zed and all its capabilities. - [Configuration](./configuration.md): Learn how to set up different language model providers like Anthropic, OpenAI, Ollama, Google AI, and more. +- [External Agents](./external-agents.md): Learn how to plug in your favorite agent into Zed. + - [Subscription](./subscription.md): Learn about Zed's hosted model service and other billing-related information. - [Privacy and Security](./privacy-and-security.md): Understand how Zed handles privacy and security with AI features. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..39d172ea5f 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2425,7 +2425,6 @@ Examples: { "completions": { "words": "fallback", - "words_min_length": 3, "lsp": true, "lsp_fetch_timeout_ms": 0, "lsp_insert_mode": "replace_suffix" @@ -2445,17 +2444,6 @@ Examples: 2. `fallback` - Only if LSP response errors or times out, use document's words to show completions 3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) -### Min Words Query Length - -- Description: 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. -- Setting: `words_min_length` -- Default: `3` - -**Options** - -Positive integer values - ### LSP - Description: Whether to fetch LSP completions or not. @@ -3243,11 +3231,9 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, - "drag_and_drop": true, "scrollbar": { "show": null }, - "sticky_scroll": true, "show_diagnostics": "all", "indent_guides": { "show": "always" diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index 9603c8197c..a015fbebf8 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -51,7 +51,7 @@ To configure, use ```json5 "project_panel": { - "show_diagnostics": "all", + "diagnostics": "all", } ``` diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index ef4b026db1..6f530433bd 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -299,7 +299,6 @@ To run tests in your Ruby project, you can set up custom tasks in your local `.z "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -322,7 +321,6 @@ Plain minitest does not support running tests by line number, only by name, so w "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -336,7 +334,6 @@ Plain minitest does not support running tests by line number, only by name, so w "label": "test $ZED_RELATIVE_FILE:$ZED_ROW", "command": "bundle", "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -372,7 +369,7 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name "label": "Debug Rails server", "adapter": "rdbg", "request": "launch", - "command": "./bin/rails", + "command": "$ZED_WORKTREE_ROOT/bin/rails", "args": ["server"], "cwd": "$ZED_WORKTREE_ROOT", "env": { diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 0bfa3ecac7..7695280275 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -136,7 +136,22 @@ This is enabled by default and can be configured as ## Manual Cargo Diagnostics fetch By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. +For lager projects this might introduce excessive wait times, so a more fine-grained triggering could be enabled by altering the + +```json +"diagnostics": { + "cargo": { + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false + } +} +``` + +default settings. + +This will stop rust-analyzer from running `cargo check ...` on save, yet still allow to run +`editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. ## More server configuration diff --git a/docs/src/tasks.md b/docs/src/tasks.md index bff3eac860..9550563432 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true + "show_output": true, // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 4fc5a9ba88..3ad1e381d9 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -430,8 +430,6 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "indent_size": 20, // Pixels for each successive indent "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir - "sticky_scroll": true, // Stick parent directories at top of the project panel. - "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, diff --git a/script/squawk b/script/squawk index 497fcff089..8489206f14 100755 --- a/script/squawk +++ b/script/squawk @@ -15,11 +15,13 @@ SQUAWK_VERSION=0.26.0 SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" -pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license -# When bootstrapping a brand new CI machine, the `target` directory may not exist yet. -mkdir -p "./target" -curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" -chmod +x "$SQUAWK_BIN" +if [ ! -f "$SQUAWK_BIN" ]; then + pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license + # When bootstrapping a brand new CI machine, the `target` directory may not exist yet. + mkdir -p "./target" + curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" + chmod +x "$SQUAWK_BIN" +fi if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 2f9a963abc..054e757056 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -54,7 +54,6 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -109,6 +108,7 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } +schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -183,7 +183,6 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -243,6 +242,7 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } +schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -403,6 +403,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -443,6 +444,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -481,6 +483,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -521,6 +524,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -606,6 +610,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -646,6 +651,7 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } +event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }